Compare commits
	
		
			52 Commits
		
	
	
		
			2eb1f4b52b
			...
			2.1.1+39
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 06dd3e092a | |||
| 82fe9e287a | |||
| dc1c285de1 | |||
| 5a3313e94f | |||
| 61032c84f1 | |||
| 36a5b8fb39 | |||
| 3eda464e03 | |||
| 7a3ab6fd7d | |||
| 3d15c0b9f9 | |||
| 67a29b4305 | |||
| 594f57e0d3 | |||
| d1eb51c596 | |||
| 85d2eff7f8 | |||
| 2375c46852 | |||
| fd2eb5cda6 | |||
| 1256f440bd | |||
| 5b05ca67b6 | |||
| 95af7140cd | |||
| 77e9994204 | |||
| 3f6c186c13 | |||
| 9ac4a940dd | |||
| ec050ab712 | |||
| 77e3ce8bcc | |||
| f5dcf71e10 | |||
| 7fc18b40db | |||
| 8c8ab24c9e | |||
| a319bd7f8c | |||
| 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 | 
							
								
								
									
										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. |  | ||||||
| @@ -10,14 +10,22 @@ plugins { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|     implementation "androidx.glance:glance:1.1.1" |     implementation 'com.google.android.material:material:1.12.0' | ||||||
|     implementation "androidx.glance:glance-appwidget:1.1.1" |     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 'androidx.compose.foundation:foundation-layout-android:1.7.6' | ||||||
|     implementation 'com.google.code.gson:gson:2.10.1' |     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-compose:3.0.4' | ||||||
|     implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.4' |     implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.4' | ||||||
| } | } | ||||||
|  |  | ||||||
|  | def keystoreProperties = new Properties() | ||||||
|  | def keystorePropertiesFile = rootProject.file('key.properties') | ||||||
|  | if (keystorePropertiesFile.exists()) { | ||||||
|  |     keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) | ||||||
|  | } | ||||||
|  |  | ||||||
| android { | android { | ||||||
|     buildFeatures { |     buildFeatures { | ||||||
|         compose true |         compose true | ||||||
| @@ -48,11 +56,25 @@ android { | |||||||
|         versionName = flutter.versionName |         versionName = flutter.versionName | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     buildTypes { |     signingConfigs { | ||||||
|         release { |         release { | ||||||
|             // TODO: Add your own signing config for the release build. |             keyAlias = keystoreProperties['keyAlias'] | ||||||
|             // Signing with the debug keys for now, so `flutter run --release` works. |             keyPassword = keystoreProperties['keyPassword'] | ||||||
|             signingConfig = signingConfigs.debug |             storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null | ||||||
|  |             storePassword = keystoreProperties['storePassword'] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     buildTypes { | ||||||
|  |         debug { | ||||||
|  |             debuggable true | ||||||
|  |  | ||||||
|  |             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||||
|  |         } | ||||||
|  |         release { | ||||||
|  |             signingConfig = signingConfigs.release | ||||||
|  |  | ||||||
|  |             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:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" | ||||||
|             android:hardwareAccelerated="true" |             android:hardwareAccelerated="true" | ||||||
|             android:windowSoftInputMode="adjustResize"> |             android:windowSoftInputMode="adjustResize"> | ||||||
|  |             <!-- Widgets Indents --> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="es.antonborri.home_widget.action.LAUNCH" /> | ||||||
|  |             </intent-filter> | ||||||
|  |  | ||||||
|             <!-- Sharing Intents --> |             <!-- 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> |             <intent-filter> | ||||||
|                 <action android:name="android.intent.action.SEND" /> |                 <action android:name="android.intent.action.SEND" /> | ||||||
|                 <category android:name="android.intent.category.DEFAULT" /> |                 <category android:name="android.intent.category.DEFAULT" /> | ||||||
| @@ -100,15 +89,15 @@ | |||||||
|                 android:name="android.appwidget.provider" |                 android:name="android.appwidget.provider" | ||||||
|                 android:resource="@xml/check_in_widget" /> |                 android:resource="@xml/check_in_widget" /> | ||||||
|         </receiver> |         </receiver> | ||||||
|         <receiver android:name=".widgets.FeaturedPostWidgetReceiver" |         <receiver android:name=".widgets.RandomPostWidgetReceiver" | ||||||
|             android:label="Featured Post" |             android:label="Random Post" | ||||||
|             android:exported="true"> |             android:exported="true"> | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> |                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|             <meta-data |             <meta-data | ||||||
|                 android:name="android.appwidget.provider" |                 android:name="android.appwidget.provider" | ||||||
|                 android:resource="@xml/featured_post_widget" /> |                 android:resource="@xml/random_post_widget" /> | ||||||
|         </receiver> |         </receiver> | ||||||
|     </application> |     </application> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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>) | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| package dev.solsynth.solian.data | package dev.solsynth.solian.data | ||||||
|  |  | ||||||
|  | import androidx.annotation.Keep | ||||||
| import java.time.Instant | import java.time.Instant | ||||||
|  |  | ||||||
|  | @Keep | ||||||
| data class SolarPost( | data class SolarPost( | ||||||
|     val id: Int, |     val id: Int, | ||||||
|     val body: SolarPostBody, |     val body: SolarPostBody, | ||||||
| @@ -13,13 +15,14 @@ data class SolarPost( | |||||||
|     val publishedAt: Instant? |     val publishedAt: Instant? | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @Keep | ||||||
| data class SolarPostBody( | data class SolarPostBody( | ||||||
|     val content: String?, |     val content: String?, | ||||||
|     val title: String?, |     val title: String?, | ||||||
|     val description: String?, |     val description: String?, | ||||||
|     val attachments: List<String>? |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @Keep | ||||||
| data class SolarPublisher( | data class SolarPublisher( | ||||||
|     val id: Int, |     val id: Int, | ||||||
|     val name: String, |     val name: String, | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| package dev.solsynth.solian.data | package dev.solsynth.solian.data | ||||||
|  |  | ||||||
|  | import androidx.annotation.Keep | ||||||
| import com.google.gson.JsonDeserializationContext | import com.google.gson.JsonDeserializationContext | ||||||
| import com.google.gson.JsonDeserializer | import com.google.gson.JsonDeserializer | ||||||
| import com.google.gson.JsonElement | import com.google.gson.JsonElement | ||||||
| @@ -11,7 +12,7 @@ import java.lang.reflect.Type | |||||||
| import java.time.Instant | import java.time.Instant | ||||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||||
|  |  | ||||||
|  | @Keep | ||||||
| class InstantAdapter : JsonSerializer<Instant?>, | class InstantAdapter : JsonSerializer<Instant?>, | ||||||
|     JsonDeserializer<Instant?> { |     JsonDeserializer<Instant?> { | ||||||
|     override fun serialize( |     override fun serialize( | ||||||
|   | |||||||
| @@ -1,13 +1,16 @@ | |||||||
| package dev.solsynth.solian.data | package dev.solsynth.solian.data | ||||||
|  |  | ||||||
|  | import androidx.annotation.Keep | ||||||
| import java.time.Instant | import java.time.Instant | ||||||
|  |  | ||||||
|  | @Keep | ||||||
| data class SolarUser( | data class SolarUser( | ||||||
|     val id: Int, |     val id: Int, | ||||||
|     val name: String, |     val name: String, | ||||||
|     val nick: String |     val nick: String | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @Keep | ||||||
| data class SolarCheckInRecord( | data class SolarCheckInRecord( | ||||||
|     val id: Int, |     val id: Int, | ||||||
|     val resultTier: Int, |     val resultTier: Int, | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| import android.content.Context | import android.content.Context | ||||||
|  | import android.net.Uri | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.ui.graphics.Color |  | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.compose.ui.unit.sp | import androidx.compose.ui.unit.sp | ||||||
| import androidx.glance.Button |  | ||||||
| import androidx.glance.GlanceId | import androidx.glance.GlanceId | ||||||
| import androidx.glance.GlanceModifier | import androidx.glance.GlanceModifier | ||||||
|  | import androidx.glance.GlanceTheme | ||||||
|  | import androidx.glance.action.clickable | ||||||
| import androidx.glance.appwidget.GlanceAppWidget | import androidx.glance.appwidget.GlanceAppWidget | ||||||
| import androidx.glance.appwidget.provideContent | import androidx.glance.appwidget.provideContent | ||||||
| import androidx.glance.background | import androidx.glance.background | ||||||
| @@ -14,19 +15,22 @@ import androidx.glance.layout.Alignment | |||||||
| import androidx.glance.layout.Column | import androidx.glance.layout.Column | ||||||
| import androidx.glance.layout.Row | import androidx.glance.layout.Row | ||||||
| import androidx.glance.layout.Spacer | import androidx.glance.layout.Spacer | ||||||
|  | import androidx.glance.layout.fillMaxHeight | ||||||
| import androidx.glance.layout.fillMaxWidth | import androidx.glance.layout.fillMaxWidth | ||||||
| import androidx.glance.layout.height | import androidx.glance.layout.height | ||||||
| import androidx.glance.layout.padding | import androidx.glance.layout.padding | ||||||
| import androidx.glance.state.GlanceStateDefinition | import androidx.glance.state.GlanceStateDefinition | ||||||
| import androidx.glance.text.FontFamily | import androidx.glance.text.FontFamily | ||||||
| import androidx.glance.text.FontWeight |  | ||||||
| import androidx.glance.text.Text | import androidx.glance.text.Text | ||||||
| import androidx.glance.text.TextStyle | import androidx.glance.text.TextStyle | ||||||
| import com.google.gson.FieldNamingPolicy | import com.google.gson.FieldNamingPolicy | ||||||
| import com.google.gson.GsonBuilder | import com.google.gson.GsonBuilder | ||||||
|  | import dev.solsynth.solian.MainActivity | ||||||
| import dev.solsynth.solian.data.InstantAdapter | import dev.solsynth.solian.data.InstantAdapter | ||||||
| import dev.solsynth.solian.data.SolarCheckInRecord | import dev.solsynth.solian.data.SolarCheckInRecord | ||||||
|  | import es.antonborri.home_widget.actionStartActivity | ||||||
| import java.time.Instant | import java.time.Instant | ||||||
|  | import java.time.LocalDate | ||||||
| import java.time.OffsetDateTime | import java.time.OffsetDateTime | ||||||
| import java.time.ZoneId | import java.time.ZoneId | ||||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||||
| @@ -37,7 +41,9 @@ class CheckInWidget : GlanceAppWidget() { | |||||||
|  |  | ||||||
|     override suspend fun provideGlance(context: Context, id: GlanceId) { |     override suspend fun provideGlance(context: Context, id: GlanceId) { | ||||||
|         provideContent { |         provideContent { | ||||||
|             GlanceContent(context, currentState()) |             GlanceTheme { | ||||||
|  |                 GlanceContent(context, currentState()) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -51,47 +57,72 @@ class CheckInWidget : GlanceAppWidget() { | |||||||
|         val resultTierSymbols = listOf("大凶", "凶", "中平", "吉", "大吉") |         val resultTierSymbols = listOf("大凶", "凶", "中平", "吉", "大吉") | ||||||
|  |  | ||||||
|         val prefs = currentState.preferences |         val prefs = currentState.preferences | ||||||
|         val checkInRaw = prefs.getString("today_check_in", null) |         val checkInRaw: String? = prefs.getString("pas_check_in_record", null) | ||||||
|  |  | ||||||
|  |         val checkIn: SolarCheckInRecord? = | ||||||
|  |             checkInRaw?.let { checkInRaw -> | ||||||
|  |                 gson.fromJson(checkInRaw, SolarCheckInRecord::class.java) | ||||||
|  |             } ?: null; | ||||||
|  |  | ||||||
|         Column( |         Column( | ||||||
|             modifier = GlanceModifier |             modifier = GlanceModifier | ||||||
|                 .fillMaxWidth() |                 .fillMaxWidth() | ||||||
|                 .background(Color.White) |                 .fillMaxHeight() | ||||||
|  |                 .background(GlanceTheme.colors.widgetBackground) | ||||||
|                 .padding(16.dp) |                 .padding(16.dp) | ||||||
|  |                 .clickable( | ||||||
|  |                     onClick = actionStartActivity<MainActivity>( | ||||||
|  |                         context, | ||||||
|  |                         Uri.parse("https://sn.solsynth.dev") | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|         ) { |         ) { | ||||||
|             if (checkInRaw != null) { |             if (checkIn != null) { | ||||||
|                 val checkIn = gson.fromJson(checkInRaw, SolarCheckInRecord::class.java) |  | ||||||
|                 val dateFormatter = DateTimeFormatter.ofPattern("EEE, MM/dd") |                 val dateFormatter = DateTimeFormatter.ofPattern("EEE, MM/dd") | ||||||
|  |  | ||||||
|                 Column { |                 val checkDate = checkIn.createdAt.atZone(ZoneId.of("UTC")).toLocalDate() | ||||||
|                     Text( |                 val currentDate = LocalDate.now() | ||||||
|                         text = resultTierSymbols[checkIn.resultTier], |                 if (checkDate.isEqual(currentDate)) { | ||||||
|                         style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif) |                     Column { | ||||||
|                     ) |                         Text( | ||||||
|                     Text( |                             text = resultTierSymbols[checkIn.resultTier], | ||||||
|                         text = "+${checkIn.resultExperience} EXP", |                             style = TextStyle( | ||||||
|                         style = TextStyle(fontSize = 15.sp, fontFamily = FontFamily.Monospace) |                                 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; | ||||||
|                 } |                 } | ||||||
|                 Spacer(modifier = GlanceModifier.height(8.dp)) |  | ||||||
|                 Row(horizontalAlignment = Alignment.CenterHorizontally) { |  | ||||||
|                     Text( |  | ||||||
|                         text = OffsetDateTime.ofInstant(checkIn.createdAt, ZoneId.systemDefault()) |  | ||||||
|                             .format(dateFormatter), |  | ||||||
|                         style = TextStyle(fontSize = 13.sp) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 Text( |  | ||||||
|                     text = "You haven't checked in today", |  | ||||||
|                     style = TextStyle(fontSize = 15.sp) |  | ||||||
|                 ) |  | ||||||
|                 Spacer(modifier = GlanceModifier.height(8.dp)) |  | ||||||
|                 Button( |  | ||||||
|                     text = "Check In", |  | ||||||
|                     onClick = {} |  | ||||||
|                 ) |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             Text( | ||||||
|  |                 text = "You haven't checked in today", | ||||||
|  |                 style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface) | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,160 +0,0 @@ | |||||||
| import android.content.Context |  | ||||||
| import androidx.compose.runtime.Composable |  | ||||||
| import androidx.compose.ui.graphics.Color |  | ||||||
| import androidx.compose.ui.text.style.TextOverflow |  | ||||||
| import androidx.compose.ui.unit.dp |  | ||||||
| import androidx.compose.ui.unit.sp |  | ||||||
| import androidx.glance.GlanceId |  | ||||||
| import androidx.glance.GlanceModifier |  | ||||||
| import androidx.glance.appwidget.GlanceAppWidget |  | ||||||
| import androidx.glance.appwidget.provideContent |  | ||||||
| import androidx.glance.background |  | ||||||
| import androidx.glance.currentState |  | ||||||
| import androidx.glance.layout.Alignment |  | ||||||
| import androidx.glance.layout.Column |  | ||||||
| import androidx.glance.layout.Row |  | ||||||
| import androidx.glance.layout.Spacer |  | ||||||
| import androidx.glance.layout.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 coil3.ImageLoader |  | ||||||
| import coil3.compose.AsyncImage |  | ||||||
| import coil3.compose.setSingletonImageLoaderFactory |  | ||||||
| import coil3.request.crossfade |  | ||||||
| import com.google.gson.FieldNamingPolicy |  | ||||||
| import com.google.gson.GsonBuilder |  | ||||||
| import dev.solsynth.solian.data.InstantAdapter |  | ||||||
| import dev.solsynth.solian.data.SolarPost |  | ||||||
| import java.time.Instant |  | ||||||
| import java.time.LocalDateTime |  | ||||||
| import java.time.ZoneId |  | ||||||
| import java.time.format.DateTimeFormatter |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class FeaturedPostWidget : GlanceAppWidget() { |  | ||||||
|     override val stateDefinition: GlanceStateDefinition<*>? |  | ||||||
|         get() = HomeWidgetGlanceStateDefinition() |  | ||||||
|  |  | ||||||
|     override suspend fun provideGlance(context: Context, id: GlanceId) { |  | ||||||
|         provideContent { |  | ||||||
|             GlanceContent(context, currentState()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private val serverUrl = "https://api.sn.solsynth.dev" |  | ||||||
|  |  | ||||||
|     private fun getAttachmentUrl(identifier: String): String { |  | ||||||
|         return if (identifier.startsWith("http")) { |  | ||||||
|             identifier |  | ||||||
|         } else { |  | ||||||
|             "$serverUrl/cgi/uc/attachments/$identifier" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Composable |  | ||||||
|     private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { |  | ||||||
|         setSingletonImageLoaderFactory { context -> |  | ||||||
|             ImageLoader.Builder(context) |  | ||||||
|                 .crossfade(true) |  | ||||||
|                 .build() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         val gson = |  | ||||||
|             GsonBuilder() |  | ||||||
|                 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) |  | ||||||
|                 .registerTypeAdapter(Instant::class.java, InstantAdapter()) |  | ||||||
|                 .create() |  | ||||||
|  |  | ||||||
|         val prefs = currentState.preferences |  | ||||||
|         val postFeaturedRaw = prefs.getString("post_featured", null) |  | ||||||
|  |  | ||||||
|         Column( |  | ||||||
|             modifier = GlanceModifier |  | ||||||
|                 .fillMaxWidth() |  | ||||||
|                 .background(Color.White) |  | ||||||
|                 .padding(16.dp) |  | ||||||
|         ) { |  | ||||||
|             if (postFeaturedRaw != null) { |  | ||||||
|                 val postFeaturedList: Array<SolarPost> = |  | ||||||
|                     gson.fromJson(postFeaturedRaw, Array<SolarPost>::class.java) |  | ||||||
|                 val postFeatured = postFeaturedList.firstOrNull(); |  | ||||||
|  |  | ||||||
|                 Row { |  | ||||||
|                     Text( |  | ||||||
|                         text = postFeatured?.publisher?.nick ?: "Unknown", |  | ||||||
|                         style = TextStyle(fontSize = 15.sp) |  | ||||||
|                     ) |  | ||||||
|                     Spacer(modifier = GlanceModifier.width(8.dp)) |  | ||||||
|                     Text( |  | ||||||
|                         text = "@${postFeatured?.publisher?.name}", |  | ||||||
|                         style = TextStyle(fontSize = 13.sp, fontFamily = FontFamily.Monospace) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 Spacer(modifier = GlanceModifier.height(8.dp)) |  | ||||||
|  |  | ||||||
|                 if (postFeatured?.body?.title != null) { |  | ||||||
|                     Text( |  | ||||||
|                         text = postFeatured.body.title, |  | ||||||
|                         style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|                 if (postFeatured?.body?.description != null) { |  | ||||||
|                     Text( |  | ||||||
|                         text = postFeatured.body.description, |  | ||||||
|                         style = TextStyle(fontSize = 19.sp, fontFamily = FontFamily.Serif) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (postFeatured?.body?.title != null || postFeatured?.body?.description != null) { |  | ||||||
|                     Spacer(modifier = GlanceModifier.height(8.dp)) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 Text( |  | ||||||
|                     text = postFeatured?.body?.content ?: "No content", |  | ||||||
|                     style = TextStyle(fontSize = 15.sp), |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 Spacer(modifier = GlanceModifier.height(8.dp)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|                 if (postFeatured?.createdAt != null) { |  | ||||||
|                     Text( |  | ||||||
|                         LocalDateTime.ofInstant(postFeatured.createdAt, ZoneId.systemDefault()) |  | ||||||
|                             .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), |  | ||||||
|                         style = TextStyle(fontSize = 13.sp), |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 Text( |  | ||||||
|                     "Solar Network Featured Post", |  | ||||||
|                     style = TextStyle(fontSize = 11.sp, fontWeight = FontWeight.Bold), |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 return@Column; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             Column( |  | ||||||
|                 modifier = GlanceModifier.fillMaxSize(), |  | ||||||
|                 verticalAlignment = Alignment.Vertical.CenterVertically, |  | ||||||
|                 horizontalAlignment = Alignment.Horizontal.CenterHorizontally |  | ||||||
|             ) { |  | ||||||
|                 Text( |  | ||||||
|                     text = "No featured posts", |  | ||||||
|                     style = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold) |  | ||||||
|                 ) |  | ||||||
|                 Text( |  | ||||||
|                     text = "Open the app to load recommendations", |  | ||||||
|                     style = TextStyle(fontSize = 15.sp) |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| package dev.solsynth.solian.widgets |  | ||||||
|  |  | ||||||
| import FeaturedPostWidget |  | ||||||
| import HomeWidgetGlanceWidgetReceiver |  | ||||||
|  |  | ||||||
| class FeaturedPostWidgetReceiver : HomeWidgetGlanceWidgetReceiver<FeaturedPostWidget>() { |  | ||||||
|     override val glanceAppWidget = FeaturedPostWidget() |  | ||||||
| } |  | ||||||
| @@ -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() | ||||||
|  | } | ||||||
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 537 B | 
| Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 372 B | 
| Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 736 B | 
| Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.5 KiB | 
| @@ -1,4 +1,5 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <resources> | <resources> | ||||||
|   <color name="ic_launcher_background">#FFFFFFFF</color> |   <color name="ic_launcher_background">#FFFFFFFF</color> | ||||||
|  |   <color name="ic_notification_background">#00000000</color> | ||||||
| </resources> | </resources> | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <resources> | <resources> | ||||||
|     <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> |     <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> | ||||||
|     <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar"> |     <style name="LaunchTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> | ||||||
|         <!-- Show a splash screen on the activity. Automatically removed when |         <!-- Show a splash screen on the activity. Automatically removed when | ||||||
|              the Flutter engine draws its first frame --> |              the Flutter engine draws its first frame --> | ||||||
|         <item name="android:windowBackground">@drawable/launch_background</item> |         <item name="android:windowBackground">@drawable/launch_background</item> | ||||||
| @@ -16,7 +16,7 @@ | |||||||
|          running. |          running. | ||||||
|  |  | ||||||
|          This Theme is only used starting with V2 of Flutter's Android embedding. --> |          This Theme is only used starting with V2 of Flutter's Android embedding. --> | ||||||
|     <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> |     <style name="NormalTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> | ||||||
|         <item name="android:windowBackground">?android:colorBackground</item> |         <item name="android:windowBackground">?android:colorBackground</item> | ||||||
|     </style> |     </style> | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" | <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     android:initialLayout="@layout/glance_default_loading_layout" |     android:initialLayout="@layout/glance_default_loading_layout" | ||||||
|     android:minWidth="80dp" |     android:minWidth="40dp" | ||||||
|     android:minHeight="40dp" |     android:minHeight="40dp" | ||||||
|     android:resizeMode="horizontal|vertical" |     android:resizeMode="horizontal|vertical" | ||||||
|     android:updatePeriodMillis="10000"> |     android:updatePeriodMillis="10000"> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" | <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     android:initialLayout="@layout/glance_default_loading_layout" |     android:initialLayout="@layout/glance_default_loading_layout" | ||||||
|     android:minWidth="320dp" |     android:minWidth="240dp" | ||||||
|     android:minHeight="40dp" |     android:minHeight="40dp" | ||||||
|     android:resizeMode="horizontal|vertical" |     android:resizeMode="horizontal|vertical" | ||||||
|     android:updatePeriodMillis="10000"> |     android:updatePeriodMillis="10000"> | ||||||
							
								
								
									
										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() |         google() | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|     } |     } | ||||||
|  |     configurations.all { | ||||||
|  |         resolutionStrategy { | ||||||
|  |             eachDependency { | ||||||
|  |                 if ((requested.group == "androidx.work") && (requested.name.startsWith("work-runtime"))) { | ||||||
|  |                     useVersion("2.9.1") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| rootProject.buildDir = "../build" | rootProject.buildDir = "../build" | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError | ||||||
| android.useAndroidX=true | android.useAndroidX=true | ||||||
| android.enableJetifier=true | android.enableJetifier=true | ||||||
| kotlin.suppressKotlinVersionCompatibilityCheck=true |  | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ | |||||||
|   "reply": "Reply", |   "reply": "Reply", | ||||||
|   "unset": "Unset", |   "unset": "Unset", | ||||||
|   "untitled": "Untitled", |   "untitled": "Untitled", | ||||||
|   "postDetail": "Post detail", |   "postDetail": "Post Detail", | ||||||
|   "postNoun": "Post", |   "postNoun": "Post", | ||||||
|   "postReadMore": "Read more", |   "postReadMore": "Read more", | ||||||
|   "postReadEstimate": "Est read time {}", |   "postReadEstimate": "Est read time {}", | ||||||
| @@ -139,6 +139,9 @@ | |||||||
|   "fieldPostTitle": "Title", |   "fieldPostTitle": "Title", | ||||||
|   "fieldPostDescription": "Description", |   "fieldPostDescription": "Description", | ||||||
|   "fieldPostTags": "Tags", |   "fieldPostTags": "Tags", | ||||||
|  |   "fieldPostCategories": "Categories", | ||||||
|  |   "fieldPostAlias": "Alias", | ||||||
|  |   "fieldPostAliasHint": "Optional, used to represent the post in URL, should follow URL-Safe.", | ||||||
|   "postPublish": "Publish", |   "postPublish": "Publish", | ||||||
|   "postPosted": "Post has been posted.", |   "postPosted": "Post has been posted.", | ||||||
|   "postPublishedAt": "Published At", |   "postPublishedAt": "Published At", | ||||||
| @@ -176,12 +179,18 @@ | |||||||
|     "other": "{} comments" |     "other": "{} comments" | ||||||
|   }, |   }, | ||||||
|   "settingsAppearance": "Appearance", |   "settingsAppearance": "Appearance", | ||||||
|  |   "settingsAppBarTransparent": "Transparent App Bar", | ||||||
|  |   "settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.", | ||||||
|   "settingsBackgroundImage": "Background Image", |   "settingsBackgroundImage": "Background Image", | ||||||
|   "settingsBackgroundImageDescription": "Set the background image that will be applied globally.", |   "settingsBackgroundImageDescription": "Set the background image that will be applied globally.", | ||||||
|   "settingsBackgroundImageClear": "Clear Existing Background Image", |   "settingsBackgroundImageClear": "Clear Existing Background Image", | ||||||
|   "settingsBackgroundImageClearDescription": "Reset the background image to blank.", |   "settingsBackgroundImageClearDescription": "Reset the background image to blank.", | ||||||
|   "settingsThemeMaterial3": "Use Material You Design", |   "settingsThemeMaterial3": "Use Material You Design", | ||||||
|   "settingsThemeMaterial3Description": "Set the application theme to Material 3 Design.", |   "settingsThemeMaterial3Description": "Set the application theme to Material 3 Design.", | ||||||
|  |   "settingsColorScheme": "Color Scheme", | ||||||
|  |   "settingsColorSchemeDescription": "Set the application primary color.", | ||||||
|  |   "settingsColorSeed": "Color Seed", | ||||||
|  |   "settingsColorSeedDescription": "Select one of the present color schemes.", | ||||||
|   "settingsNetwork": "Network", |   "settingsNetwork": "Network", | ||||||
|   "settingsNetworkServer": "HyperNet Server", |   "settingsNetworkServer": "HyperNet Server", | ||||||
|   "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", |   "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", | ||||||
| @@ -190,6 +199,13 @@ | |||||||
|   "settingsNetworkServerPreset": "Present HyperNet Server", |   "settingsNetworkServerPreset": "Present HyperNet Server", | ||||||
|   "settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.", |   "settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.", | ||||||
|   "settingsNetworkServerSaved": "Server address saved.", |   "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", |   "settingsMisc": "Misc", | ||||||
|   "settingsMiscAbout": "About", |   "settingsMiscAbout": "About", | ||||||
|   "settingsMiscAboutDescription": "View the version information of Solian.", |   "settingsMiscAboutDescription": "View the version information of Solian.", | ||||||
| @@ -362,7 +378,26 @@ | |||||||
|   "dailyCheckNegativeHint5Description": "Lost connection at a crucial moment", |   "dailyCheckNegativeHint5Description": "Lost connection at a crucial moment", | ||||||
|   "dailyCheckNegativeHint6": "Going out", |   "dailyCheckNegativeHint6": "Going out", | ||||||
|   "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", |   "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", | ||||||
|   "happyBirthday": "Happy birthday, {}!", |   "celebrateBirthday": "Happy birthday, {}!", | ||||||
|  |   "celebrateMerryXmas": "Merry christmas, {}!", | ||||||
|  |   "celebrateNewYear": "Happy new year, {}!", | ||||||
|  |   "celebrateValentineDay": "Today is valentine's day, {}!", | ||||||
|  |   "celebrateLaborDay": "Today is labor day, {}.", | ||||||
|  |   "celebrateMotherDay": "Today is mother's day, {}.", | ||||||
|  |   "celebrateChildrenDay": "Today is children's day, {}!", | ||||||
|  |   "celebrateFatherDay": "Today is father's day, {}.", | ||||||
|  |   "celebrateHalloween": "Happy halloween, {}!", | ||||||
|  |   "celebrateThanksgiving": "Today is thanksgiving day, {}!", | ||||||
|  |   "pendingBirthday": "Birthday in {}", | ||||||
|  |   "pendingMerryXmas": "Christmas in {}", | ||||||
|  |   "pendingNewYear": "New year in {}", | ||||||
|  |   "pendingValentineDay": "Valentine's day in {}", | ||||||
|  |   "pendingLaborDay": "Labor day in {}", | ||||||
|  |   "pendingMotherDay": "Mother's day in {}", | ||||||
|  |   "pendingChildrenDay": "Children's day in {}", | ||||||
|  |   "pendingFatherDay": "Father's day in {}", | ||||||
|  |   "pendingHalloween": "Halloween in {}", | ||||||
|  |   "pendingThanksgiving": "Thanksgiving day in {}", | ||||||
|   "friendNew": "Add Friend", |   "friendNew": "Add Friend", | ||||||
|   "friendRequests": "Friend Requests", |   "friendRequests": "Friend Requests", | ||||||
|   "friendRequestsDescription": { |   "friendRequestsDescription": { | ||||||
| @@ -439,7 +474,7 @@ | |||||||
|   "publisherBlockHintDescription": "You are going to block this publisher's maintainer, this will also block publishers that run by the same user.", |   "publisherBlockHintDescription": "You are going to block this publisher's maintainer, this will also block publishers that run by the same user.", | ||||||
|   "userUnblocked": "{} has been unblocked.", |   "userUnblocked": "{} has been unblocked.", | ||||||
|   "userBlocked": "{} has been blocked.", |   "userBlocked": "{} has been blocked.", | ||||||
|   "postSharingViaPicture": "Capturing post as picture, please stand by...", |   "postSharingViaPicture": "Capturing post as picture, please wait...", | ||||||
|   "postImageShareReadMore": "Scan the QR code to read full post", |   "postImageShareReadMore": "Scan the QR code to read full post", | ||||||
|   "postImageShareAds": "Explore posts on the Solar Network", |   "postImageShareAds": "Explore posts on the Solar Network", | ||||||
|   "postShare": "Share", |   "postShare": "Share", | ||||||
| @@ -448,5 +483,28 @@ | |||||||
|   "poweredBy": "Powered by {}", |   "poweredBy": "Powered by {}", | ||||||
|   "shareIntent": "Share", |   "shareIntent": "Share", | ||||||
|   "shareIntentDescription":  "What do you want to do with the content you are sharing?", |   "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": "Updating, please wait...", | ||||||
|  |   "custom": "Custom", | ||||||
|  |   "colorSchemeIndigo": "Indigo", | ||||||
|  |   "colorSchemeBlue": "Blue", | ||||||
|  |   "colorSchemeGreen": "Green", | ||||||
|  |   "colorSchemeYellow": "Yellow", | ||||||
|  |   "colorSchemeOrange": "Orange", | ||||||
|  |   "colorSchemeRed": "Red", | ||||||
|  |   "colorSchemeWhite": "White", | ||||||
|  |   "colorSchemeBlack": "Black", | ||||||
|  |   "colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.", | ||||||
|  |   "postCategoryTechnology": "Technology", | ||||||
|  |   "postCategoryGaming": "Gaming", | ||||||
|  |   "postCategoryLife": "Life", | ||||||
|  |   "postCategoryArts": "Arts", | ||||||
|  |   "postCategorySports": "Sports", | ||||||
|  |   "postCategoryMusic": "Music", | ||||||
|  |   "postCategoryNews": "News", | ||||||
|  |   "postCategoryKnowledge": "Knowledge", | ||||||
|  |   "postCategoryLiterature": "Literature", | ||||||
|  |   "postCategoryFunny": "Funny", | ||||||
|  |   "postCategoryUncategorized": "Uncategorized" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -123,6 +123,9 @@ | |||||||
|   "fieldPostTitle": "标题", |   "fieldPostTitle": "标题", | ||||||
|   "fieldPostDescription": "描述", |   "fieldPostDescription": "描述", | ||||||
|   "fieldPostTags": "标签", |   "fieldPostTags": "标签", | ||||||
|  |   "fieldPostCategories": "分类", | ||||||
|  |   "fieldPostAlias": "别名", | ||||||
|  |   "fieldPostAliasHint": "可选项,用于在 URL 中表示该帖子,应遵循 URL-Safe 的原则。", | ||||||
|   "postPublish": "发布", |   "postPublish": "发布", | ||||||
|   "postPublishedAt": "发布于", |   "postPublishedAt": "发布于", | ||||||
|   "postPublishedUntil": "取消发布于", |   "postPublishedUntil": "取消发布于", | ||||||
| @@ -180,6 +183,12 @@ | |||||||
|   "settingsBackgroundImageClearDescription": "将应用背景图重置为空白。", |   "settingsBackgroundImageClearDescription": "将应用背景图重置为空白。", | ||||||
|   "settingsThemeMaterial3": "使用 Material You 设计范式", |   "settingsThemeMaterial3": "使用 Material You 设计范式", | ||||||
|   "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。", |   "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。", | ||||||
|  |   "settingsAppBarTransparent": "透明顶栏", | ||||||
|  |   "settingsAppBarTransparentDescription": "为顶栏启用透明效果。", | ||||||
|  |   "settingsColorScheme": "主题色", | ||||||
|  |   "settingsColorSchemeDescription": "设置应用主题色。", | ||||||
|  |   "settingsColorSeed": "预设色彩主题", | ||||||
|  |   "settingsColorSeedDescription": "选择一个预设色彩主题。", | ||||||
|   "settingsNetwork": "网络", |   "settingsNetwork": "网络", | ||||||
|   "settingsNetworkServer": "HyperNet 服务器", |   "settingsNetworkServer": "HyperNet 服务器", | ||||||
|   "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", | ||||||
| @@ -188,6 +197,13 @@ | |||||||
|   "settingsNetworkServerPreset": "预设的 HyperNet 服务器", |   "settingsNetworkServerPreset": "预设的 HyperNet 服务器", | ||||||
|   "settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。", |   "settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。", | ||||||
|   "settingsNetworkServerSaved": "服务器地址已保存。", |   "settingsNetworkServerSaved": "服务器地址已保存。", | ||||||
|  |   "settingsPerformance": "性能", | ||||||
|  |   "settingsImageQuality": "图片预览质量", | ||||||
|  |   "settingsImageQualityDescription": "设置图片预览质量,会影响图片解码速度。", | ||||||
|  |   "settingsImageQualityLowest": "极低", | ||||||
|  |   "settingsImageQualityLow": "低", | ||||||
|  |   "settingsImageQualityMedium": "中", | ||||||
|  |   "settingsImageQualityHigh": "高", | ||||||
|   "settingsMisc": "杂项", |   "settingsMisc": "杂项", | ||||||
|   "settingsMiscAbout": "关于", |   "settingsMiscAbout": "关于", | ||||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", |   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||||
| @@ -360,7 +376,26 @@ | |||||||
|   "dailyCheckNegativeHint5Description": "关键时刻断网", |   "dailyCheckNegativeHint5Description": "关键时刻断网", | ||||||
|   "dailyCheckNegativeHint6": "出门", |   "dailyCheckNegativeHint6": "出门", | ||||||
|   "dailyCheckNegativeHint6Description": "忘带伞遇上大雨", |   "dailyCheckNegativeHint6Description": "忘带伞遇上大雨", | ||||||
|   "happyBirthday": "生日快乐,{}!", |   "celebrateBirthday": "生日快乐,{}!", | ||||||
|  |   "celebrateMerryXmas": "圣诞快乐,{}!", | ||||||
|  |   "celebrateNewYear": "新年快乐,{}!", | ||||||
|  |   "celebrateValentineDay": "今天是情人节,{}!", | ||||||
|  |   "celebrateLaborDay": "今天是劳动节,{}。", | ||||||
|  |   "celebrateMotherDay": "今天是母亲节,{}。", | ||||||
|  |   "celebrateChildrenDay": "今天是儿童节,{}!", | ||||||
|  |   "celebrateFatherDay": "今天是父亲节,{}。", | ||||||
|  |   "celebrateHalloween": "快乐在圣诞节,{}!", | ||||||
|  |   "celebrateThanksgiving": "今天是感恩节,{}!", | ||||||
|  |   "pendingBirthday": "{} 过生日", | ||||||
|  |   "pendingMerryXmas": "{} 过圣诞节", | ||||||
|  |   "pendingNewYear": "{} 跨年", | ||||||
|  |   "pendingValentineDay": "{} 过情人节", | ||||||
|  |   "pendingLaborDay": "{} 过劳动节", | ||||||
|  |   "pendingMotherDay": "{} 过母亲节", | ||||||
|  |   "pendingChildrenDay": "{} 过儿童节", | ||||||
|  |   "pendingFatherDay": "{} 过父亲节", | ||||||
|  |   "pendingHalloween": "{} 过圣诞节", | ||||||
|  |   "pendingThanksgiving": "{} 过感恩节", | ||||||
|   "friendNew": "添加好友", |   "friendNew": "添加好友", | ||||||
|   "friendRequests": "好友请求", |   "friendRequests": "好友请求", | ||||||
|   "friendRequestsDescription": { |   "friendRequestsDescription": { | ||||||
| @@ -399,7 +434,7 @@ | |||||||
|   "accountStatus": "状态", |   "accountStatus": "状态", | ||||||
|   "accountStatusOnline": "在线", |   "accountStatusOnline": "在线", | ||||||
|   "accountStatusOffline": "离线", |   "accountStatusOffline": "离线", | ||||||
|   "accountStatusLastSeen": "最后一次在 {} 上线", |   "accountStatusLastSeen": "最后一次上线于 {}", | ||||||
|   "postArticle": "Solar Network 上的文章", |   "postArticle": "Solar Network 上的文章", | ||||||
|   "postStory": "Solar Network 上的故事", |   "postStory": "Solar Network 上的故事", | ||||||
|   "articleWrittenAt": "发表于 {}", |   "articleWrittenAt": "发表于 {}", | ||||||
| @@ -446,5 +481,28 @@ | |||||||
|   "poweredBy": "由 {} 提供支持", |   "poweredBy": "由 {} 提供支持", | ||||||
|   "shareIntent": "分享", |   "shareIntent": "分享", | ||||||
|   "shareIntentDescription": "您想对您分享的内容做些什么?", |   "shareIntentDescription": "您想对您分享的内容做些什么?", | ||||||
|   "shareIntentPostStory": "发布动态" |   "shareIntentPostStory": "发布动态", | ||||||
|  |   "updateAvailable": "检测到更新可用", | ||||||
|  |   "updateOngoing": "正在更新,请稍后……", | ||||||
|  |   "custom": "自定义", | ||||||
|  |   "colorSchemeIndigo": "靛蓝", | ||||||
|  |   "colorSchemeBlue": "蓝色", | ||||||
|  |   "colorSchemeGreen": "绿色", | ||||||
|  |   "colorSchemeYellow": "黄色", | ||||||
|  |   "colorSchemeOrange": "橙色", | ||||||
|  |   "colorSchemeRed": "红色", | ||||||
|  |   "colorSchemeWhite": "白色", | ||||||
|  |   "colorSchemeBlack": "黑色", | ||||||
|  |   "colorSchemeApplied": "主题色已应用,可能需要重启来生效。", | ||||||
|  |   "postCategoryTechnology": "技术", | ||||||
|  |   "postCategoryGaming": "游戏", | ||||||
|  |   "postCategoryLife": "生活", | ||||||
|  |   "postCategoryArts": "艺术", | ||||||
|  |   "postCategorySports": "体育", | ||||||
|  |   "postCategoryMusic": "音乐", | ||||||
|  |   "postCategoryNews": "新闻", | ||||||
|  |   "postCategoryKnowledge": "知识", | ||||||
|  |   "postCategoryLiterature": "文学", | ||||||
|  |   "postCategoryFunny": "搞笑", | ||||||
|  |   "postCategoryUncategorized": "未分类" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -123,6 +123,9 @@ | |||||||
|   "fieldPostTitle": "標題", |   "fieldPostTitle": "標題", | ||||||
|   "fieldPostDescription": "描述", |   "fieldPostDescription": "描述", | ||||||
|   "fieldPostTags": "標籤", |   "fieldPostTags": "標籤", | ||||||
|  |   "fieldPostCategories": "分類", | ||||||
|  |   "fieldPostAlias": "別名", | ||||||
|  |   "fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。", | ||||||
|   "postPublish": "發佈", |   "postPublish": "發佈", | ||||||
|   "postPublishedAt": "發佈於", |   "postPublishedAt": "發佈於", | ||||||
|   "postPublishedUntil": "取消發佈於", |   "postPublishedUntil": "取消發佈於", | ||||||
| @@ -180,6 +183,12 @@ | |||||||
|   "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。", |   "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。", | ||||||
|   "settingsThemeMaterial3": "使用 Material You 設計範式", |   "settingsThemeMaterial3": "使用 Material You 設計範式", | ||||||
|   "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", |   "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", | ||||||
|  |   "settingsAppBarTransparent": "透明頂欄", | ||||||
|  |   "settingsAppBarTransparentDescription": "為頂欄啓用透明效果。", | ||||||
|  |   "settingsColorScheme": "主題色", | ||||||
|  |   "settingsColorSchemeDescription": "設置應用主題色。", | ||||||
|  |   "settingsColorSeed": "預設色彩主題", | ||||||
|  |   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||||
|   "settingsNetwork": "網絡", |   "settingsNetwork": "網絡", | ||||||
|   "settingsNetworkServer": "HyperNet 服務器", |   "settingsNetworkServer": "HyperNet 服務器", | ||||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||||
| @@ -188,6 +197,13 @@ | |||||||
|   "settingsNetworkServerPreset": "預設的 HyperNet 服務器", |   "settingsNetworkServerPreset": "預設的 HyperNet 服務器", | ||||||
|   "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 服務器地址。", |   "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 服務器地址。", | ||||||
|   "settingsNetworkServerSaved": "服務器地址已保存。", |   "settingsNetworkServerSaved": "服務器地址已保存。", | ||||||
|  |   "settingsPerformance": "性能", | ||||||
|  |   "settingsImageQuality": "圖片預覽質量", | ||||||
|  |   "settingsImageQualityDescription": "設置圖片預覽質量,會影響圖片解碼速度。", | ||||||
|  |   "settingsImageQualityLowest": "極低", | ||||||
|  |   "settingsImageQualityLow": "低", | ||||||
|  |   "settingsImageQualityMedium": "中", | ||||||
|  |   "settingsImageQualityHigh": "高", | ||||||
|   "settingsMisc": "雜項", |   "settingsMisc": "雜項", | ||||||
|   "settingsMiscAbout": "關於", |   "settingsMiscAbout": "關於", | ||||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", |   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||||
| @@ -360,7 +376,26 @@ | |||||||
|   "dailyCheckNegativeHint5Description": "關鍵時刻斷網", |   "dailyCheckNegativeHint5Description": "關鍵時刻斷網", | ||||||
|   "dailyCheckNegativeHint6": "出門", |   "dailyCheckNegativeHint6": "出門", | ||||||
|   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", |   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", | ||||||
|   "happyBirthday": "生日快樂,{}!", |   "celebrateBirthday": "生日快樂,{}!", | ||||||
|  |   "celebrateMerryXmas": "聖誕快樂,{}!", | ||||||
|  |   "celebrateNewYear": "新年快樂,{}!", | ||||||
|  |   "celebrateValentineDay": "今天是情人節,{}!", | ||||||
|  |   "celebrateLaborDay": "今天是勞動節,{}。", | ||||||
|  |   "celebrateMotherDay": "今天是母親節,{}。", | ||||||
|  |   "celebrateChildrenDay": "今天是兒童節,{}!", | ||||||
|  |   "celebrateFatherDay": "今天是父親節,{}。", | ||||||
|  |   "celebrateHalloween": "快樂在聖誕節,{}!", | ||||||
|  |   "celebrateThanksgiving": "今天是感恩節,{}!", | ||||||
|  |   "pendingBirthday": "{} 過生日", | ||||||
|  |   "pendingMerryXmas": "{} 過聖誕節", | ||||||
|  |   "pendingNewYear": "{} 跨年", | ||||||
|  |   "pendingValentineDay": "{} 過情人節", | ||||||
|  |   "pendingLaborDay": "{} 過勞動節", | ||||||
|  |   "pendingMotherDay": "{} 過母親節", | ||||||
|  |   "pendingChildrenDay": "{} 過兒童節", | ||||||
|  |   "pendingFatherDay": "{} 過父親節", | ||||||
|  |   "pendingHalloween": "{} 過聖誕節", | ||||||
|  |   "pendingThanksgiving": "{} 過感恩節", | ||||||
|   "friendNew": "添加好友", |   "friendNew": "添加好友", | ||||||
|   "friendRequests": "好友請求", |   "friendRequests": "好友請求", | ||||||
|   "friendRequestsDescription": { |   "friendRequestsDescription": { | ||||||
| @@ -399,7 +434,7 @@ | |||||||
|   "accountStatus": "狀態", |   "accountStatus": "狀態", | ||||||
|   "accountStatusOnline": "在線", |   "accountStatusOnline": "在線", | ||||||
|   "accountStatusOffline": "離線", |   "accountStatusOffline": "離線", | ||||||
|   "accountStatusLastSeen": "最後一次在 {} 上線", |   "accountStatusLastSeen": "最後一次上線於 {}", | ||||||
|   "postArticle": "Solar Network 上的文章", |   "postArticle": "Solar Network 上的文章", | ||||||
|   "postStory": "Solar Network 上的故事", |   "postStory": "Solar Network 上的故事", | ||||||
|   "articleWrittenAt": "發表於 {}", |   "articleWrittenAt": "發表於 {}", | ||||||
| @@ -441,5 +476,33 @@ | |||||||
|   "postImageShareReadMore": "掃描右側 QRCode 查看全文", |   "postImageShareReadMore": "掃描右側 QRCode 查看全文", | ||||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", |   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||||
|   "postShare": "分享", |   "postShare": "分享", | ||||||
|   "postShareImage": "分享帖圖" |   "postShareImage": "分享帖圖", | ||||||
|  |   "appInitializing": "正在初始化", | ||||||
|  |   "poweredBy": "由 {} 提供支持", | ||||||
|  |   "shareIntent": "分享", | ||||||
|  |   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||||
|  |   "shareIntentPostStory": "發佈動態", | ||||||
|  |   "updateAvailable": "檢測到更新可用", | ||||||
|  |   "updateOngoing": "正在更新,請稍後……", | ||||||
|  |   "custom": "自定義", | ||||||
|  |   "colorSchemeIndigo": "靛藍", | ||||||
|  |   "colorSchemeBlue": "藍色", | ||||||
|  |   "colorSchemeGreen": "綠色", | ||||||
|  |   "colorSchemeYellow": "黃色", | ||||||
|  |   "colorSchemeOrange": "橙色", | ||||||
|  |   "colorSchemeRed": "紅色", | ||||||
|  |   "colorSchemeWhite": "白色", | ||||||
|  |   "colorSchemeBlack": "黑色", | ||||||
|  |   "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。", | ||||||
|  |   "postCategoryTechnology": "技術", | ||||||
|  |   "postCategoryGaming": "遊戲", | ||||||
|  |   "postCategoryLife": "生活", | ||||||
|  |   "postCategoryArts": "藝術", | ||||||
|  |   "postCategorySports": "體育", | ||||||
|  |   "postCategoryMusic": "音樂", | ||||||
|  |   "postCategoryNews": "新聞", | ||||||
|  |   "postCategoryKnowledge": "知識", | ||||||
|  |   "postCategoryLiterature": "文學", | ||||||
|  |   "postCategoryFunny": "搞笑", | ||||||
|  |   "postCategoryUncategorized": "未分類" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -123,6 +123,9 @@ | |||||||
|   "fieldPostTitle": "標題", |   "fieldPostTitle": "標題", | ||||||
|   "fieldPostDescription": "描述", |   "fieldPostDescription": "描述", | ||||||
|   "fieldPostTags": "標籤", |   "fieldPostTags": "標籤", | ||||||
|  |   "fieldPostCategories": "分類", | ||||||
|  |   "fieldPostAlias": "別名", | ||||||
|  |   "fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。", | ||||||
|   "postPublish": "釋出", |   "postPublish": "釋出", | ||||||
|   "postPublishedAt": "釋出於", |   "postPublishedAt": "釋出於", | ||||||
|   "postPublishedUntil": "取消釋出於", |   "postPublishedUntil": "取消釋出於", | ||||||
| @@ -180,6 +183,12 @@ | |||||||
|   "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。", |   "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。", | ||||||
|   "settingsThemeMaterial3": "使用 Material You 設計正規化", |   "settingsThemeMaterial3": "使用 Material You 設計正規化", | ||||||
|   "settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。", |   "settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。", | ||||||
|  |   "settingsAppBarTransparent": "透明頂欄", | ||||||
|  |   "settingsAppBarTransparentDescription": "為頂欄啟用透明效果。", | ||||||
|  |   "settingsColorScheme": "主題色", | ||||||
|  |   "settingsColorSchemeDescription": "設定應用主題色。", | ||||||
|  |   "settingsColorSeed": "預設色彩主題", | ||||||
|  |   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||||
|   "settingsNetwork": "網路", |   "settingsNetwork": "網路", | ||||||
|   "settingsNetworkServer": "HyperNet 伺服器", |   "settingsNetworkServer": "HyperNet 伺服器", | ||||||
|   "settingsNetworkServerDescription": "設定 HyperNet 伺服器地址,選擇我們提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "設定 HyperNet 伺服器地址,選擇我們提供的,或者自己搭建。", | ||||||
| @@ -188,6 +197,13 @@ | |||||||
|   "settingsNetworkServerPreset": "預設的 HyperNet 伺服器", |   "settingsNetworkServerPreset": "預設的 HyperNet 伺服器", | ||||||
|   "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 伺服器地址。", |   "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 伺服器地址。", | ||||||
|   "settingsNetworkServerSaved": "伺服器地址已儲存。", |   "settingsNetworkServerSaved": "伺服器地址已儲存。", | ||||||
|  |   "settingsPerformance": "效能", | ||||||
|  |   "settingsImageQuality": "圖片預覽質量", | ||||||
|  |   "settingsImageQualityDescription": "設定圖片預覽質量,會影響圖片解碼速度。", | ||||||
|  |   "settingsImageQualityLowest": "極低", | ||||||
|  |   "settingsImageQualityLow": "低", | ||||||
|  |   "settingsImageQualityMedium": "中", | ||||||
|  |   "settingsImageQualityHigh": "高", | ||||||
|   "settingsMisc": "雜項", |   "settingsMisc": "雜項", | ||||||
|   "settingsMiscAbout": "關於", |   "settingsMiscAbout": "關於", | ||||||
|   "settingsMiscAboutDescription": "檢視 Solian 的版本資訊。", |   "settingsMiscAboutDescription": "檢視 Solian 的版本資訊。", | ||||||
| @@ -360,7 +376,26 @@ | |||||||
|   "dailyCheckNegativeHint5Description": "關鍵時刻斷網", |   "dailyCheckNegativeHint5Description": "關鍵時刻斷網", | ||||||
|   "dailyCheckNegativeHint6": "出門", |   "dailyCheckNegativeHint6": "出門", | ||||||
|   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", |   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", | ||||||
|   "happyBirthday": "生日快樂,{}!", |   "celebrateBirthday": "生日快樂,{}!", | ||||||
|  |   "celebrateMerryXmas": "聖誕快樂,{}!", | ||||||
|  |   "celebrateNewYear": "新年快樂,{}!", | ||||||
|  |   "celebrateValentineDay": "今天是情人節,{}!", | ||||||
|  |   "celebrateLaborDay": "今天是勞動節,{}。", | ||||||
|  |   "celebrateMotherDay": "今天是母親節,{}。", | ||||||
|  |   "celebrateChildrenDay": "今天是兒童節,{}!", | ||||||
|  |   "celebrateFatherDay": "今天是父親節,{}。", | ||||||
|  |   "celebrateHalloween": "快樂在聖誕節,{}!", | ||||||
|  |   "celebrateThanksgiving": "今天是感恩節,{}!", | ||||||
|  |   "pendingBirthday": "{} 過生日", | ||||||
|  |   "pendingMerryXmas": "{} 過聖誕節", | ||||||
|  |   "pendingNewYear": "{} 跨年", | ||||||
|  |   "pendingValentineDay": "{} 過情人節", | ||||||
|  |   "pendingLaborDay": "{} 過勞動節", | ||||||
|  |   "pendingMotherDay": "{} 過母親節", | ||||||
|  |   "pendingChildrenDay": "{} 過兒童節", | ||||||
|  |   "pendingFatherDay": "{} 過父親節", | ||||||
|  |   "pendingHalloween": "{} 過聖誕節", | ||||||
|  |   "pendingThanksgiving": "{} 過感恩節", | ||||||
|   "friendNew": "新增好友", |   "friendNew": "新增好友", | ||||||
|   "friendRequests": "好友請求", |   "friendRequests": "好友請求", | ||||||
|   "friendRequestsDescription": { |   "friendRequestsDescription": { | ||||||
| @@ -399,7 +434,7 @@ | |||||||
|   "accountStatus": "狀態", |   "accountStatus": "狀態", | ||||||
|   "accountStatusOnline": "線上", |   "accountStatusOnline": "線上", | ||||||
|   "accountStatusOffline": "離線", |   "accountStatusOffline": "離線", | ||||||
|   "accountStatusLastSeen": "最後一次在 {} 上線", |   "accountStatusLastSeen": "最後一次上線於 {}", | ||||||
|   "postArticle": "Solar Network 上的文章", |   "postArticle": "Solar Network 上的文章", | ||||||
|   "postStory": "Solar Network 上的故事", |   "postStory": "Solar Network 上的故事", | ||||||
|   "articleWrittenAt": "發表於 {}", |   "articleWrittenAt": "發表於 {}", | ||||||
| @@ -441,5 +476,33 @@ | |||||||
|   "postImageShareReadMore": "掃描右側 QRCode 檢視全文", |   "postImageShareReadMore": "掃描右側 QRCode 檢視全文", | ||||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", |   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||||
|   "postShare": "分享", |   "postShare": "分享", | ||||||
|   "postShareImage": "分享帖圖" |   "postShareImage": "分享帖圖", | ||||||
|  |   "appInitializing": "正在初始化", | ||||||
|  |   "poweredBy": "由 {} 提供支援", | ||||||
|  |   "shareIntent": "分享", | ||||||
|  |   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||||
|  |   "shareIntentPostStory": "釋出動態", | ||||||
|  |   "updateAvailable": "檢測到更新可用", | ||||||
|  |   "updateOngoing": "正在更新,請稍後……", | ||||||
|  |   "custom": "自定義", | ||||||
|  |   "colorSchemeIndigo": "靛藍", | ||||||
|  |   "colorSchemeBlue": "藍色", | ||||||
|  |   "colorSchemeGreen": "綠色", | ||||||
|  |   "colorSchemeYellow": "黃色", | ||||||
|  |   "colorSchemeOrange": "橙色", | ||||||
|  |   "colorSchemeRed": "紅色", | ||||||
|  |   "colorSchemeWhite": "白色", | ||||||
|  |   "colorSchemeBlack": "黑色", | ||||||
|  |   "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。", | ||||||
|  |   "postCategoryTechnology": "技術", | ||||||
|  |   "postCategoryGaming": "遊戲", | ||||||
|  |   "postCategoryLife": "生活", | ||||||
|  |   "postCategoryArts": "藝術", | ||||||
|  |   "postCategorySports": "體育", | ||||||
|  |   "postCategoryMusic": "音樂", | ||||||
|  |   "postCategoryNews": "新聞", | ||||||
|  |   "postCategoryKnowledge": "知識", | ||||||
|  |   "postCategoryLiterature": "文學", | ||||||
|  |   "postCategoryFunny": "搞笑", | ||||||
|  |   "postCategoryUncategorized": "未分類" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								ios/Podfile
									
									
									
									
									
								
							
							
						
						| @@ -36,6 +36,24 @@ target 'Runner' do | |||||||
|     inherit! :search_paths |     inherit! :search_paths | ||||||
|   end |   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 |   target 'SolarShare' do | ||||||
|     inherit! :search_paths |     inherit! :search_paths | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| PODS: | PODS: | ||||||
|  |   - Alamofire (5.10.2) | ||||||
|   - connectivity_plus (0.0.1): |   - connectivity_plus (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
| @@ -56,7 +57,7 @@ PODS: | |||||||
|     - Firebase/Analytics (= 11.4.0) |     - Firebase/Analytics (= 11.4.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_core (3.8.1): |   - firebase_core (3.9.0): | ||||||
|     - Firebase/CoreOnly (= 11.4.0) |     - Firebase/CoreOnly (= 11.4.0) | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_messaging (15.1.6): |   - firebase_messaging (15.1.6): | ||||||
| @@ -102,6 +103,8 @@ PODS: | |||||||
|     - GoogleUtilities/UserDefaults (~> 8.0) |     - GoogleUtilities/UserDefaults (~> 8.0) | ||||||
|     - nanopb (~> 3.30910.0) |     - nanopb (~> 3.30910.0) | ||||||
|   - Flutter (1.0.0) |   - Flutter (1.0.0) | ||||||
|  |   - flutter_app_update (0.0.1): | ||||||
|  |     - Flutter | ||||||
|   - flutter_native_splash (2.4.3): |   - flutter_native_splash (2.4.3): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - flutter_udid (0.0.1): |   - flutter_udid (0.0.1): | ||||||
| @@ -167,6 +170,9 @@ PODS: | |||||||
|     - Flutter |     - Flutter | ||||||
|   - image_picker_ios (0.0.1): |   - image_picker_ios (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|  |   - in_app_review (2.0.0): | ||||||
|  |     - Flutter | ||||||
|  |   - Kingfisher (8.1.3) | ||||||
|   - livekit_client (2.3.2): |   - livekit_client (2.3.2): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
| @@ -216,8 +222,11 @@ PODS: | |||||||
|   - wakelock_plus (0.0.1): |   - wakelock_plus (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - WebRTC-SDK (125.6422.06) |   - WebRTC-SDK (125.6422.06) | ||||||
|  |   - workmanager (0.0.1): | ||||||
|  |     - Flutter | ||||||
|  |  | ||||||
| DEPENDENCIES: | DEPENDENCIES: | ||||||
|  |   - Alamofire | ||||||
|   - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) |   - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) | ||||||
|   - croppy (from `.symlinks/plugins/croppy/ios`) |   - croppy (from `.symlinks/plugins/croppy/ios`) | ||||||
|   - device_info_plus (from `.symlinks/plugins/device_info_plus/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_core (from `.symlinks/plugins/firebase_core/ios`) | ||||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) |   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||||
|   - Flutter (from `Flutter`) |   - Flutter (from `Flutter`) | ||||||
|  |   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) |   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||||
|   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) |   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) | ||||||
|   - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) |   - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) | ||||||
|   - gal (from `.symlinks/plugins/gal/darwin`) |   - gal (from `.symlinks/plugins/gal/darwin`) | ||||||
|   - home_widget (from `.symlinks/plugins/home_widget/ios`) |   - home_widget (from `.symlinks/plugins/home_widget/ios`) | ||||||
|   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/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`) |   - livekit_client (from `.symlinks/plugins/livekit_client/ios`) | ||||||
|   - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/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`) |   - 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`) |   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) | ||||||
|   - volume_controller (from `.symlinks/plugins/volume_controller/ios`) |   - volume_controller (from `.symlinks/plugins/volume_controller/ios`) | ||||||
|   - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) |   - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) | ||||||
|  |   - workmanager (from `.symlinks/plugins/workmanager/ios`) | ||||||
|  |  | ||||||
| SPEC REPOS: | SPEC REPOS: | ||||||
|   trunk: |   trunk: | ||||||
|  |     - Alamofire | ||||||
|     - DKImagePickerController |     - DKImagePickerController | ||||||
|     - DKPhotoGallery |     - DKPhotoGallery | ||||||
|     - Firebase |     - Firebase | ||||||
| @@ -263,6 +277,7 @@ SPEC REPOS: | |||||||
|     - GoogleAppMeasurement |     - GoogleAppMeasurement | ||||||
|     - GoogleDataTransport |     - GoogleDataTransport | ||||||
|     - GoogleUtilities |     - GoogleUtilities | ||||||
|  |     - Kingfisher | ||||||
|     - nanopb |     - nanopb | ||||||
|     - PromisesObjC |     - PromisesObjC | ||||||
|     - SAMKeychain |     - SAMKeychain | ||||||
| @@ -289,6 +304,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/firebase_messaging/ios" |     :path: ".symlinks/plugins/firebase_messaging/ios" | ||||||
|   Flutter: |   Flutter: | ||||||
|     :path: Flutter |     :path: Flutter | ||||||
|  |   flutter_app_update: | ||||||
|  |     :path: ".symlinks/plugins/flutter_app_update/ios" | ||||||
|   flutter_native_splash: |   flutter_native_splash: | ||||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" |     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||||
|   flutter_udid: |   flutter_udid: | ||||||
| @@ -301,6 +318,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/home_widget/ios" |     :path: ".symlinks/plugins/home_widget/ios" | ||||||
|   image_picker_ios: |   image_picker_ios: | ||||||
|     :path: ".symlinks/plugins/image_picker_ios/ios" |     :path: ".symlinks/plugins/image_picker_ios/ios" | ||||||
|  |   in_app_review: | ||||||
|  |     :path: ".symlinks/plugins/in_app_review/ios" | ||||||
|   livekit_client: |   livekit_client: | ||||||
|     :path: ".symlinks/plugins/livekit_client/ios" |     :path: ".symlinks/plugins/livekit_client/ios" | ||||||
|   media_kit_libs_ios_video: |   media_kit_libs_ios_video: | ||||||
| @@ -333,8 +352,11 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/volume_controller/ios" |     :path: ".symlinks/plugins/volume_controller/ios" | ||||||
|   wakelock_plus: |   wakelock_plus: | ||||||
|     :path: ".symlinks/plugins/wakelock_plus/ios" |     :path: ".symlinks/plugins/wakelock_plus/ios" | ||||||
|  |   workmanager: | ||||||
|  |     :path: ".symlinks/plugins/workmanager/ios" | ||||||
|  |  | ||||||
| SPEC CHECKSUMS: | SPEC CHECKSUMS: | ||||||
|  |   Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 | ||||||
|   connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 |   connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 | ||||||
|   croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321 |   croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321 | ||||||
|   device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 |   device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 | ||||||
| @@ -344,7 +366,7 @@ SPEC CHECKSUMS: | |||||||
|   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 |   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 | ||||||
|   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 |   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 | ||||||
|   firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75 |   firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75 | ||||||
|   firebase_core: 418aed674e9a0b8b6088aec16cde82a811f6261f |   firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10 | ||||||
|   firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9 |   firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9 | ||||||
|   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 |   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 | ||||||
|   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 |   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 | ||||||
| @@ -352,15 +374,18 @@ SPEC CHECKSUMS: | |||||||
|   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 |   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 | ||||||
|   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 |   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 | ||||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 |   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||||
|  |   flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc | ||||||
|   flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a |   flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a | ||||||
|   flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 |   flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab | ||||||
|   flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563 |   flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563 | ||||||
|   gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 |   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||||
|   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e |   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e | ||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d |   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||||
|   home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 |   home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 | ||||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 |   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 | ||||||
|  |   in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 | ||||||
|  |   Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef | ||||||
|   livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd |   livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd | ||||||
|   media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 |   media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 | ||||||
|   media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a |   media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a | ||||||
| @@ -381,9 +406,10 @@ SPEC CHECKSUMS: | |||||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 |   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||||
|   url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe |   url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe | ||||||
|   volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 |   volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 | ||||||
|   wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 |   wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 | ||||||
|   WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db |   WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db | ||||||
|  |   workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 | ||||||
|  |  | ||||||
| PODFILE CHECKSUM: 23d35ad686cacf9103d1e85035ee4f3e9750630d | PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc | ||||||
|  |  | ||||||
| COCOAPODS: 1.16.2 | COCOAPODS: 1.16.2 | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ | |||||||
| 		738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */; }; | 		738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */; }; | ||||||
| 		738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.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, ); }; }; | 		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, ); }; }; | 		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, ); }; }; | 		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 */; }; | 		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 */; }; | 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; | ||||||
| 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; | 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; | ||||||
| 		CED170BFB6A72CDDAC285637 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */; }; | 		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 */; }; | 		F51C4E3C8FA95426C91FC0A4 /* Pods_SolarShare.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */; }; | ||||||
| /* End PBXBuildFile section */ | /* End PBXBuildFile section */ | ||||||
|  |  | ||||||
| @@ -83,30 +86,40 @@ | |||||||
| /* End PBXCopyFilesBuildPhase section */ | /* End PBXCopyFilesBuildPhase section */ | ||||||
|  |  | ||||||
| /* Begin PBXFileReference 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>"; }; | 		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>"; }; | 		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; }; | 		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; }; | 		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>"; }; | 		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>"; }; | 		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; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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; }; | 		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; }; | 		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; }; | 		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>"; }; | 		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; }; | 		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; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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; }; | 		EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||||
| /* End PBXFileReference section */ | /* End PBXFileReference section */ | ||||||
|  |  | ||||||
| @@ -217,6 +235,7 @@ | |||||||
| 			files = ( | 			files = ( | ||||||
| 				738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */, | 				738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */, | ||||||
| 				738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */, | 				738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */, | ||||||
|  | 				D962B51F682FBDEC00AC7281 /* Pods_SolarWidgetExtension.framework in Frameworks */, | ||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 		}; | 		}; | ||||||
| @@ -232,6 +251,7 @@ | |||||||
| 			isa = PBXFrameworksBuildPhase; | 			isa = PBXFrameworksBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| 			files = ( | 			files = ( | ||||||
|  | 				D5125CF12F159F0B8BC7641D /* Pods_SolarNotifyService.framework in Frameworks */, | ||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 		}; | 		}; | ||||||
| @@ -262,6 +282,8 @@ | |||||||
| 				731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */, | 				731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */, | ||||||
| 				731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */, | 				731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */, | ||||||
| 				16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */, | 				16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */, | ||||||
|  | 				02469D286F48D84300484B1E /* Pods_SolarNotifyService.framework */, | ||||||
|  | 				7B1A159F5551E280D0EFC129 /* Pods_SolarWidgetExtension.framework */, | ||||||
| 			); | 			); | ||||||
| 			name = Frameworks; | 			name = Frameworks; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -328,6 +350,7 @@ | |||||||
| 				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, | 				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, | ||||||
| 				74858FAE1ED2DC5600515810 /* AppDelegate.swift */, | 				74858FAE1ED2DC5600515810 /* AppDelegate.swift */, | ||||||
| 				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, | 				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, | ||||||
|  | 				7396A3512D16BD890095F4A8 /* NotifyDelegate.swift */, | ||||||
| 			); | 			); | ||||||
| 			path = Runner; | 			path = Runner; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -344,6 +367,18 @@ | |||||||
| 				5922A50B1231B06B92E31F20 /* Pods-SolarShare.debug.xcconfig */, | 				5922A50B1231B06B92E31F20 /* Pods-SolarShare.debug.xcconfig */, | ||||||
| 				B1763F1D7318A2745CA7EDFE /* Pods-SolarShare.release.xcconfig */, | 				B1763F1D7318A2745CA7EDFE /* Pods-SolarShare.release.xcconfig */, | ||||||
| 				2DA1B873D39B9FD33298BBCE /* Pods-SolarShare.profile.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; | 			path = Pods; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -374,6 +409,7 @@ | |||||||
| 			isa = PBXNativeTarget; | 			isa = PBXNativeTarget; | ||||||
| 			buildConfigurationList = 738C1EBA2D0D76A500A215F3 /* Build configuration list for PBXNativeTarget "SolarWidgetExtension" */; | 			buildConfigurationList = 738C1EBA2D0D76A500A215F3 /* Build configuration list for PBXNativeTarget "SolarWidgetExtension" */; | ||||||
| 			buildPhases = ( | 			buildPhases = ( | ||||||
|  | 				F2FCDA0E1BD434BF4883AFFD /* [CP] Check Pods Manifest.lock */, | ||||||
| 				738C1EA72D0D76A400A215F3 /* Sources */, | 				738C1EA72D0D76A400A215F3 /* Sources */, | ||||||
| 				738C1EA82D0D76A400A215F3 /* Frameworks */, | 				738C1EA82D0D76A400A215F3 /* Frameworks */, | ||||||
| 				738C1EA92D0D76A400A215F3 /* Resources */, | 				738C1EA92D0D76A400A215F3 /* Resources */, | ||||||
| @@ -416,6 +452,7 @@ | |||||||
| 			isa = PBXNativeTarget; | 			isa = PBXNativeTarget; | ||||||
| 			buildConfigurationList = 73DA8A072D05C7620024A03E /* Build configuration list for PBXNativeTarget "SolarNotifyService" */; | 			buildConfigurationList = 73DA8A072D05C7620024A03E /* Build configuration list for PBXNativeTarget "SolarNotifyService" */; | ||||||
| 			buildPhases = ( | 			buildPhases = ( | ||||||
|  | 				50F5704AB2E7309C916CA2E7 /* [CP] Check Pods Manifest.lock */, | ||||||
| 				73DA89F62D05C7620024A03E /* Sources */, | 				73DA89F62D05C7620024A03E /* Sources */, | ||||||
| 				73DA89F72D05C7620024A03E /* Frameworks */, | 				73DA89F72D05C7620024A03E /* Frameworks */, | ||||||
| 				73DA89F82D05C7620024A03E /* Resources */, | 				73DA89F82D05C7620024A03E /* Resources */, | ||||||
| @@ -611,6 +648,28 @@ | |||||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | ||||||
| 			showEnvVarsInLog = 0; | 			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 */ = { | 		738C1EBE2D0D76C500A215F3 /* Copy Bundle Version */ = { | ||||||
| 			isa = PBXShellScriptBuildPhase; | 			isa = PBXShellScriptBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			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"; | 			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; | 			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 */ = { | 		FC4815D44D909666EB1FA614 /* [CP] Embed Pods Frameworks */ = { | ||||||
| 			isa = PBXShellScriptBuildPhase; | 			isa = PBXShellScriptBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| @@ -765,6 +846,7 @@ | |||||||
| 			files = ( | 			files = ( | ||||||
| 				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, | 				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, | ||||||
| 				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, | 				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, | ||||||
|  | 				7396A3522D16BD890095F4A8 /* NotifyDelegate.swift in Sources */, | ||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 		}; | 		}; | ||||||
| @@ -879,7 +961,7 @@ | |||||||
| 				INFOPLIST_FILE = Runner/Info.plist; | 				INFOPLIST_FILE = Runner/Info.plist; | ||||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 12.0; | 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | 				LD_RUNPATH_SEARCH_PATHS = ( | ||||||
| 					"$(inherited)", | 					"$(inherited)", | ||||||
| 					"@executable_path/Frameworks", | 					"@executable_path/Frameworks", | ||||||
| @@ -947,6 +1029,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		738C1EBB2D0D76A500A215F3 /* Debug */ = { | 		738C1EBB2D0D76A500A215F3 /* Debug */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = 2134F3903A0E8EB8CC2670BE /* Pods-SolarWidgetExtension.debug.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||||
| @@ -990,6 +1073,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		738C1EBC2D0D76A500A215F3 /* Release */ = { | 		738C1EBC2D0D76A500A215F3 /* Release */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = 6618E2E3015264643175B43D /* Pods-SolarWidgetExtension.release.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||||
| @@ -1030,6 +1114,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		738C1EBD2D0D76A500A215F3 /* Profile */ = { | 		738C1EBD2D0D76A500A215F3 /* Profile */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = BCE0C4086B776A27B202B373 /* Pods-SolarWidgetExtension.profile.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||||
| @@ -1193,6 +1278,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		73DA8A032D05C7620024A03E /* Debug */ = { | 		73DA8A032D05C7620024A03E /* Debug */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = D96D1DB4ED46A2640C1B9D34 /* Pods-SolarNotifyService.debug.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||||
| @@ -1234,6 +1320,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		73DA8A042D05C7620024A03E /* Release */ = { | 		73DA8A042D05C7620024A03E /* Release */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = D7E1FA77FDA53439DB2C0E75 /* Pods-SolarNotifyService.release.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||||
| @@ -1272,6 +1359,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		73DA8A052D05C7620024A03E /* Profile */ = { | 		73DA8A052D05C7620024A03E /* Profile */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = 4CBF45ABD292EE527D0A4D1E /* Pods-SolarNotifyService.profile.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||||
| @@ -1433,7 +1521,7 @@ | |||||||
| 				INFOPLIST_FILE = Runner/Info.plist; | 				INFOPLIST_FILE = Runner/Info.plist; | ||||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 12.0; | 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | 				LD_RUNPATH_SEARCH_PATHS = ( | ||||||
| 					"$(inherited)", | 					"$(inherited)", | ||||||
| 					"@executable_path/Frameworks", | 					"@executable_path/Frameworks", | ||||||
| @@ -1461,7 +1549,7 @@ | |||||||
| 				INFOPLIST_FILE = Runner/Info.plist; | 				INFOPLIST_FILE = Runner/Info.plist; | ||||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 12.0; | 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | 				LD_RUNPATH_SEARCH_PATHS = ( | ||||||
| 					"$(inherited)", | 					"$(inherited)", | ||||||
| 					"@executable_path/Frameworks", | 					"@executable_path/Frameworks", | ||||||
|   | |||||||
| @@ -1,14 +1,26 @@ | |||||||
| import Flutter | import Flutter | ||||||
| import UIKit | import UIKit | ||||||
|  |  | ||||||
|  | import workmanager | ||||||
|  |  | ||||||
| @main | @main | ||||||
| @objc class AppDelegate: FlutterAppDelegate { | @objc class AppDelegate: FlutterAppDelegate { | ||||||
|   override func application( |     let notifyDelegate = NotifyDelegate() | ||||||
|     _ application: UIApplication, |      | ||||||
|     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? |     override func application( | ||||||
|   ) -> Bool { |         _ application: UIApplication, | ||||||
|     GeneratedPluginRegistrant.register(with: self) |         didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? | ||||||
|        |     ) -> Bool { | ||||||
|     return super.application(application, didFinishLaunchingWithOptions: launchOptions) |         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
									
								
							
							
						
						| @@ -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"> | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||||
| <plist version="1.0"> | <plist version="1.0"> | ||||||
| <dict> | <dict> | ||||||
|  | 	<key>AppGroupId</key> | ||||||
|  | 	<string>group.solsynth.solian</string> | ||||||
| 	<key>CADisableMinimumFrameDurationOnPhone</key> | 	<key>CADisableMinimumFrameDurationOnPhone</key> | ||||||
| 	<true/> | 	<true/> | ||||||
| 	<key>CFBundleDevelopmentRegion</key> | 	<key>CFBundleDevelopmentRegion</key> | ||||||
| @@ -27,6 +29,17 @@ | |||||||
| 	<string>$(FLUTTER_BUILD_NAME)</string> | 	<string>$(FLUTTER_BUILD_NAME)</string> | ||||||
| 	<key>CFBundleSignature</key> | 	<key>CFBundleSignature</key> | ||||||
| 	<string>????</string> | 	<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> | 	<key>CFBundleVersion</key> | ||||||
| 	<string>$(FLUTTER_BUILD_NUMBER)</string> | 	<string>$(FLUTTER_BUILD_NUMBER)</string> | ||||||
| 	<key>ITSAppUsesNonExemptEncryption</key> | 	<key>ITSAppUsesNonExemptEncryption</key> | ||||||
| @@ -34,9 +47,9 @@ | |||||||
| 	<key>LSRequiresIPhoneOS</key> | 	<key>LSRequiresIPhoneOS</key> | ||||||
| 	<true/> | 	<true/> | ||||||
| 	<key>NSCameraUsageDescription</key> | 	<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> | 	<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> | 	<key>NSPhotoLibraryAddUsageDescription</key> | ||||||
| 	<string>Grant access to Photo Library will allow Solian download photo to album for you.</string> | 	<string>Grant access to Photo Library will allow Solian download photo to album for you.</string> | ||||||
| 	<key>NSPhotoLibraryUsageDescription</key> | 	<key>NSPhotoLibraryUsageDescription</key> | ||||||
| @@ -66,8 +79,6 @@ | |||||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||||
| 		<string>UIInterfaceOrientationLandscapeRight</string> | 		<string>UIInterfaceOrientationLandscapeRight</string> | ||||||
| 	</array> | 	</array> | ||||||
| 	<key>AppGroupId</key> |  | ||||||
| 	<string>group.solsynth.solian</string> |  | ||||||
| 	<key>UISupportedInterfaceOrientations~ipad</key> | 	<key>UISupportedInterfaceOrientations~ipad</key> | ||||||
| 	<array> | 	<array> | ||||||
| 		<string>UIInterfaceOrientationPortrait</string> | 		<string>UIInterfaceOrientationPortrait</string> | ||||||
| @@ -75,16 +86,5 @@ | |||||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||||
| 		<string>UIInterfaceOrientationLandscapeRight</string> | 		<string>UIInterfaceOrientationLandscapeRight</string> | ||||||
| 	</array> | 	</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> | </dict> | ||||||
| </plist> | </plist> | ||||||
|   | |||||||
							
								
								
									
										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 UserNotifications | ||||||
| import Intents | import Intents | ||||||
|  | import Kingfisher | ||||||
|  | import UniformTypeIdentifiers | ||||||
|  |  | ||||||
| enum ParseNotificationPayloadError: Error { | enum ParseNotificationPayloadError: Error { | ||||||
|     case missingMetadata(String) |     case missingMetadata(String) | ||||||
| @@ -18,58 +20,6 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|     private var contentHandler: ((UNNotificationContent) -> Void)? |     private var contentHandler: ((UNNotificationContent) -> Void)? | ||||||
|     private var bestAttemptContent: UNMutableNotificationContent? |     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( |     override func didReceive( | ||||||
|         _ request: UNNotificationRequest, |         _ request: UNNotificationRequest, | ||||||
|         withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void |         withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void | ||||||
| @@ -112,16 +62,43 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|             throw ParseNotificationPayloadError.missingAvatarUrl("The notification has no avatar.") |             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) |         let avatarUrl = getAttachmentUrl(for: avatarIdentifier) | ||||||
|         fetchAvatarImage(from: avatarUrl) { [weak self] inImage in |          | ||||||
|             guard let self = self else { return } |         let targetSize = 640 | ||||||
|  |         let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) | ||||||
|  |          | ||||||
|  |         KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, options: [.processor(scaleProcessor)], 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( |             let sender = INPerson( | ||||||
|                 personHandle: handle, |                 personHandle: handle, | ||||||
|                 nameComponents: nil, |                 nameComponents: nil, | ||||||
|                 displayName: content.title, |                 displayName: content.title, | ||||||
|                 image: inImage, |                 image: image == nil ? nil : INImage(imageData: image!), | ||||||
|                 contactIdentifier: nil, |                 contactIdentifier: nil, | ||||||
|                 customIdentifier: nil |                 customIdentifier: nil | ||||||
|             ) |             ) | ||||||
| @@ -132,12 +109,12 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|                 let updatedContent = try? request.content.updating(from: intent) |                 let updatedContent = try? request.content.updating(from: intent) | ||||||
|                 self.contentHandler?(updatedContent ?? content) |                 self.contentHandler?(updatedContent ?? content) | ||||||
|             } else { |             } 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) |                 self.donateInteraction(for: intent) | ||||||
|                 let updatedContent = try? request.content.updating(from: intent) |                 let updatedContent = try? request.content.updating(from: intent) | ||||||
|                 self.contentHandler?(updatedContent ?? content) |                 self.contentHandler?(updatedContent ?? content) | ||||||
|             } |             } | ||||||
|         } |         }) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     private func handleDefaultNotification(content: UNMutableNotificationContent) throws { |     private func handleDefaultNotification(content: UNMutableNotificationContent) throws { | ||||||
| @@ -146,15 +123,15 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|         } |         } | ||||||
|          |          | ||||||
|         if let imageIdentifier = metadata["image"] as? String { |         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 { |         } 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) |         let attachmentUrl = getAttachmentUrl(for: identifier) | ||||||
|          |          | ||||||
|         guard let remoteUrl = URL(string: attachmentUrl) else { |         guard let remoteUrl = URL(string: attachmentUrl) else { | ||||||
| @@ -162,49 +139,62 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|             return |             return | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Define a cache location based on the identifier |         let targetSize = 800 | ||||||
|         let tempDirectory = FileManager.default.temporaryDirectory |         let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) | ||||||
|         let cachedFileUrl = tempDirectory.appendingPathComponent(identifier) |  | ||||||
|          |          | ||||||
|         if FileManager.default.fileExists(atPath: cachedFileUrl.path) { |         KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [ | ||||||
|             // Use cached file |             .processor(scaleProcessor) | ||||||
|             attachLocalMedia(to: content, from: cachedFileUrl, withIdentifier: identifier) |         ] : nil) { [weak self] result in | ||||||
|         } else { |             guard let self = self else { return } | ||||||
|             // Download and cache the file |              | ||||||
|             let session = URLSession(configuration: .default) |             switch result { | ||||||
|             session.downloadTask(with: remoteUrl) { [weak content] localUrl, response, error in |             case .success(let retrievalResult): | ||||||
|                 guard let content = content else { return } |                 // The image is either retrieved from cache or downloaded | ||||||
|                  |                 let tempDirectory = FileManager.default.temporaryDirectory | ||||||
|                 if let error = error { |                 let cachedFileUrl = tempDirectory.appendingPathComponent(identifier) | ||||||
|                     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 |  | ||||||
|                 } |  | ||||||
|                  |                  | ||||||
|                 do { |                 do { | ||||||
|                     // Move the downloaded file to the cache |                     // Write the image data to a temporary file for UNNotificationAttachment | ||||||
|                     try FileManager.default.moveItem(at: localUrl, to: cachedFileUrl) |                     try retrievalResult.image.pngData()?.write(to: cachedFileUrl) | ||||||
|                     self.attachLocalMedia(to: content, from: cachedFileUrl, withIdentifier: identifier) |                     self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: identifier) | ||||||
|                 } catch { |                 } catch { | ||||||
|                     print("Failed to cache media file: \(error.localizedDescription)") |                     print("Failed to write media to temporary file: \(error.localizedDescription)") | ||||||
|                     self.contentHandler?(content) |                     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) { |     private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) { | ||||||
|         if let attachment = try? UNNotificationAttachment(identifier: identifier, url: localUrl) { |         do { | ||||||
|  |             let attachment = try UNNotificationAttachment(identifier: identifier, url: localUrl, options: [ | ||||||
|  |                 UNNotificationAttachmentOptionsTypeHintKey: type as Any, | ||||||
|  |                 UNNotificationAttachmentOptionsThumbnailHiddenKey: 0, | ||||||
|  |             ]) | ||||||
|             content.attachments = [attachment] |             content.attachments = [attachment] | ||||||
|         } else { |         } catch let error as NSError { | ||||||
|             print("Failed to create attachment from cached file: \(localUrl.path)") |             // 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) |         self.contentHandler?(content) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import SwiftUI | |||||||
|  |  | ||||||
| struct CheckInProvider: TimelineProvider { | struct CheckInProvider: TimelineProvider { | ||||||
|     func placeholder(in context: Context) -> CheckInEntry { |     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) -> ()) { |     func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) { | ||||||
| @@ -23,21 +23,17 @@ struct CheckInProvider: TimelineProvider { | |||||||
|         jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter) |         jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter) | ||||||
|         jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase |         jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|          |          | ||||||
|         let userRaw = prefs?.string(forKey: "user") |         let checkInRaw = prefs?.string(forKey: "pas_check_in_record") | ||||||
|         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") |  | ||||||
|         var checkIn: SolarCheckInRecord? |         var checkIn: SolarCheckInRecord? | ||||||
|         if let checkInRaw = checkInRaw { |         if let checkInRaw = checkInRaw { | ||||||
|             checkIn = try! jsonDecoder.decode(SolarCheckInRecord.self, from: checkInRaw.data(using: .utf8)!) |             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( |         let entry = CheckInEntry( | ||||||
|             date: Date(), |             date: Date(), | ||||||
|             user: user, |  | ||||||
|             checkIn: checkIn |             checkIn: checkIn | ||||||
|         ) |         ) | ||||||
|         completion(entry) |         completion(entry) | ||||||
| @@ -53,7 +49,6 @@ struct CheckInProvider: TimelineProvider { | |||||||
|  |  | ||||||
| struct CheckInEntry: TimelineEntry { | struct CheckInEntry: TimelineEntry { | ||||||
|     let date: Date |     let date: Date | ||||||
|     let user: SolarUser? |  | ||||||
|     let checkIn: SolarCheckInRecord? |     let checkIn: SolarCheckInRecord? | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -105,7 +100,7 @@ struct CheckInWidgetEntryView : View { | |||||||
|                     Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing) |                     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) { | #Preview(as: .systemSmall) { | ||||||
|     CheckInWidget() |     CheckInWidget() | ||||||
| } timeline: { | } timeline: { | ||||||
|     CheckInEntry(date: .now, user: nil, checkIn: nil) |     CheckInEntry(date: .now, checkIn: nil) | ||||||
|     CheckInEntry( |     CheckInEntry( | ||||||
|         date: .now, |         date: .now, | ||||||
|         user: SolarUser(id: 1, name: "demo", nick: "Deemo"), |  | ||||||
|         checkIn: SolarCheckInRecord(id: 1, resultTier: 1, resultExperience: 100, createdAt: Date.now) |         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
									
								
							
							
						
						| @@ -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 { | struct SolarWidgetBundle: WidgetBundle { | ||||||
|     var body: some Widget { |     var body: some Widget { | ||||||
|         CheckInWidget() |         CheckInWidget() | ||||||
|         FeaturedPostWidget() |         RandomPostWidget() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  | import 'dart:math' as math; | ||||||
|  |  | ||||||
| import 'package:dio/dio.dart'; | import 'package:dio/dio.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| @@ -152,6 +153,7 @@ class PostWriteController extends ChangeNotifier { | |||||||
|   final TextEditingController contentController = TextEditingController(); |   final TextEditingController contentController = TextEditingController(); | ||||||
|   final TextEditingController titleController = TextEditingController(); |   final TextEditingController titleController = TextEditingController(); | ||||||
|   final TextEditingController descriptionController = TextEditingController(); |   final TextEditingController descriptionController = TextEditingController(); | ||||||
|  |   final TextEditingController aliasController = TextEditingController(); | ||||||
|  |  | ||||||
|   PostWriteController() { |   PostWriteController() { | ||||||
|     titleController.addListener(() => notifyListeners()); |     titleController.addListener(() => notifyListeners()); | ||||||
| @@ -176,6 +178,7 @@ class PostWriteController extends ChangeNotifier { | |||||||
|   List<int> visibleUsers = List.empty(); |   List<int> visibleUsers = List.empty(); | ||||||
|   List<int> invisibleUsers = List.empty(); |   List<int> invisibleUsers = List.empty(); | ||||||
|   List<String> tags = List.empty(); |   List<String> tags = List.empty(); | ||||||
|  |   List<String> categories = List.empty(); | ||||||
|   PostWriteMedia? thumbnail; |   PostWriteMedia? thumbnail; | ||||||
|   List<PostWriteMedia> attachments = List.empty(growable: true); |   List<PostWriteMedia> attachments = List.empty(growable: true); | ||||||
|   DateTime? publishedAt, publishedUntil; |   DateTime? publishedAt, publishedUntil; | ||||||
| @@ -198,12 +201,14 @@ class PostWriteController extends ChangeNotifier { | |||||||
|         titleController.text = post.body['title'] ?? ''; |         titleController.text = post.body['title'] ?? ''; | ||||||
|         descriptionController.text = post.body['description'] ?? ''; |         descriptionController.text = post.body['description'] ?? ''; | ||||||
|         contentController.text = post.body['content'] ?? ''; |         contentController.text = post.body['content'] ?? ''; | ||||||
|  |         aliasController.text = post.alias ?? ''; | ||||||
|         publishedAt = post.publishedAt; |         publishedAt = post.publishedAt; | ||||||
|         publishedUntil = post.publishedUntil; |         publishedUntil = post.publishedUntil; | ||||||
|         visibleUsers = List.from(post.visibleUsersList ?? []); |         visibleUsers = List.from(post.visibleUsersList ?? []); | ||||||
|         invisibleUsers = List.from(post.invisibleUsersList ?? []); |         invisibleUsers = List.from(post.invisibleUsersList ?? []); | ||||||
|         visibility = post.visibility; |         visibility = post.visibility; | ||||||
|         tags = List.from(post.tags.map((ele) => ele.alias)); |         tags = List.from(post.tags.map((ele) => ele.alias)); | ||||||
|  |         categories = List.from(post.categories.map((ele) => ele.alias)); | ||||||
|         attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); |         attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); | ||||||
|  |  | ||||||
|         if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { |         if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { | ||||||
| @@ -269,7 +274,7 @@ class PostWriteController extends ChangeNotifier { | |||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> post(BuildContext context) async { |   Future<void> sendPost(BuildContext context) async { | ||||||
|     if (isBusy || publisher == null) return; |     if (isBusy || publisher == null) return; | ||||||
|  |  | ||||||
|     final sn = context.read<SnNetworkProvider>(); |     final sn = context.read<SnNetworkProvider>(); | ||||||
| @@ -305,12 +310,14 @@ class PostWriteController extends ChangeNotifier { | |||||||
|           place.$2, |           place.$2, | ||||||
|           onProgress: (progress) { |           onProgress: (progress) { | ||||||
|             // Calculate overall progress for attachments |             // Calculate overall progress for attachments | ||||||
|             progress = ((i + progress) / attachments.length) * kAttachmentProgressWeight; |             progress = math.max(((i + progress) / attachments.length) * kAttachmentProgressWeight, progress); | ||||||
|             notifyListeners(); |             notifyListeners(); | ||||||
|           }, |           }, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         progress = (i + 1) / attachments.length * kAttachmentProgressWeight; | ||||||
|         attachments[i] = PostWriteMedia(item); |         attachments[i] = PostWriteMedia(item); | ||||||
|  |         notifyListeners(); | ||||||
|       } |       } | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       isBusy = false; |       isBusy = false; | ||||||
| @@ -334,11 +341,13 @@ class PostWriteController extends ChangeNotifier { | |||||||
|         data: { |         data: { | ||||||
|           'publisher': publisher!.id, |           'publisher': publisher!.id, | ||||||
|           'content': contentController.text, |           'content': contentController.text, | ||||||
|  |           if (aliasController.text.isNotEmpty) 'alias': aliasController.text, | ||||||
|           if (titleController.text.isNotEmpty) 'title': titleController.text, |           if (titleController.text.isNotEmpty) 'title': titleController.text, | ||||||
|           if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, |           if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, | ||||||
|           if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid, |           if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid, | ||||||
|           'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), |           'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), | ||||||
|           'tags': tags.map((ele) => {'alias': ele}).toList(), |           'tags': tags.map((ele) => {'alias': ele}).toList(), | ||||||
|  |           'categories': categories.map((ele) => {'alias': ele}).toList(), | ||||||
|           'visibility': visibility, |           'visibility': visibility, | ||||||
|           'visible_users_list': visibleUsers, |           'visible_users_list': visibleUsers, | ||||||
|           'invisible_users_list': invisibleUsers, |           'invisible_users_list': invisibleUsers, | ||||||
| @@ -425,6 +434,11 @@ class PostWriteController extends ChangeNotifier { | |||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void setCategories(List<String> value) { | ||||||
|  |     categories = value; | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void setVisibility(int value) { |   void setVisibility(int value) { | ||||||
|     visibility = value; |     visibility = value; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
| @@ -461,6 +475,9 @@ class PostWriteController extends ChangeNotifier { | |||||||
|     titleController.clear(); |     titleController.clear(); | ||||||
|     descriptionController.clear(); |     descriptionController.clear(); | ||||||
|     contentController.clear(); |     contentController.clear(); | ||||||
|  |     aliasController.clear(); | ||||||
|  |     tags.clear(); | ||||||
|  |     categories.clear(); | ||||||
|     attachments.clear(); |     attachments.clear(); | ||||||
|     editingPost = null; |     editingPost = null; | ||||||
|     replyingPost = null; |     replyingPost = null; | ||||||
| @@ -474,6 +491,7 @@ class PostWriteController extends ChangeNotifier { | |||||||
|     contentController.dispose(); |     contentController.dispose(); | ||||||
|     titleController.dispose(); |     titleController.dispose(); | ||||||
|     descriptionController.dispose(); |     descriptionController.dispose(); | ||||||
|  |     aliasController.dispose(); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										123
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						| @@ -1,8 +1,10 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
|  | import 'dart:developer'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||||
| import 'package:croppy/croppy.dart'; | import 'package:croppy/croppy.dart'; | ||||||
|  | import 'package:dio/dio.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:easy_localization_loader/easy_localization_loader.dart'; | import 'package:easy_localization_loader/easy_localization_loader.dart'; | ||||||
| import 'package:firebase_core/firebase_core.dart'; | import 'package:firebase_core/firebase_core.dart'; | ||||||
| @@ -11,13 +13,16 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
|  | import 'package:package_info_plus/package_info_plus.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:relative_time/relative_time.dart'; | import 'package:relative_time/relative_time.dart'; | ||||||
| import 'package:responsive_framework/responsive_framework.dart'; | import 'package:responsive_framework/responsive_framework.dart'; | ||||||
|  | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/firebase_options.dart'; | import 'package:surface/firebase_options.dart'; | ||||||
| import 'package:surface/providers/channel.dart'; | import 'package:surface/providers/channel.dart'; | ||||||
| import 'package:surface/providers/chat_call.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/link_preview.dart'; | ||||||
| import 'package:surface/providers/navigation.dart'; | import 'package:surface/providers/navigation.dart'; | ||||||
| import 'package:surface/providers/notification.dart'; | import 'package:surface/providers/notification.dart'; | ||||||
| @@ -25,6 +30,7 @@ import 'package:surface/providers/post.dart'; | |||||||
| import 'package:surface/providers/relationship.dart'; | import 'package:surface/providers/relationship.dart'; | ||||||
| import 'package:surface/providers/sn_attachment.dart'; | import 'package:surface/providers/sn_attachment.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/providers/special_day.dart'; | ||||||
| import 'package:surface/providers/theme.dart'; | import 'package:surface/providers/theme.dart'; | ||||||
| import 'package:surface/providers/user_directory.dart'; | import 'package:surface/providers/user_directory.dart'; | ||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
| @@ -36,7 +42,26 @@ import 'package:surface/types/realm.dart'; | |||||||
| import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; | import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/version_label.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 { | void main() async { | ||||||
|   WidgetsFlutterBinding.ensureInitialized(); |   WidgetsFlutterBinding.ensureInitialized(); | ||||||
| @@ -64,6 +89,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()); |   runApp(const SolianApp()); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -86,15 +127,18 @@ class SolianApp extends StatelessWidget { | |||||||
|         assetLoader: JsonAssetLoader(), |         assetLoader: JsonAssetLoader(), | ||||||
|         child: MultiProvider( |         child: MultiProvider( | ||||||
|           providers: [ |           providers: [ | ||||||
|  |             // System extensions layer | ||||||
|  |             Provider(create: (ctx) => HomeWidgetProvider(ctx)), | ||||||
|  |  | ||||||
|  |             // Preferences layer | ||||||
|  |             ChangeNotifierProvider(create: (ctx) => ConfigProvider(ctx)), | ||||||
|  |  | ||||||
|             // Display layer |             // Display layer | ||||||
|             ChangeNotifierProvider(create: (_) => ThemeProvider()), |             ChangeNotifierProvider(create: (_) => ThemeProvider()), | ||||||
|             ChangeNotifierProvider(create: (ctx) => NavigationProvider()), |             ChangeNotifierProvider(create: (ctx) => NavigationProvider()), | ||||||
|  |  | ||||||
|             // System extensions layer |  | ||||||
|             Provider(create: (ctx) => HomeWidgetProvider(ctx)), |  | ||||||
|  |  | ||||||
|             // Data layer |             // Data layer | ||||||
|             Provider(create: (_) => SnNetworkProvider()), |             Provider(create: (ctx) => SnNetworkProvider(ctx)), | ||||||
|             Provider(create: (ctx) => UserDirectoryProvider(ctx)), |             Provider(create: (ctx) => UserDirectoryProvider(ctx)), | ||||||
|             Provider(create: (ctx) => SnAttachmentProvider(ctx)), |             Provider(create: (ctx) => SnAttachmentProvider(ctx)), | ||||||
|             Provider(create: (ctx) => SnPostContentProvider(ctx)), |             Provider(create: (ctx) => SnPostContentProvider(ctx)), | ||||||
| @@ -105,6 +149,9 @@ class SolianApp extends StatelessWidget { | |||||||
|             ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), |             ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), | ||||||
|             ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), |             ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), | ||||||
|             ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), |             ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), | ||||||
|  |  | ||||||
|  |             // Additional helper layer | ||||||
|  |             Provider(create: (ctx) => SpecialDayProvider(ctx)), | ||||||
|           ], |           ], | ||||||
|           child: _AppDelegate(), |           child: _AppDelegate(), | ||||||
|         ), |         ), | ||||||
| @@ -163,15 +210,66 @@ class _AppSplashScreen extends StatefulWidget { | |||||||
| class _AppSplashScreenState extends State<_AppSplashScreen> { | class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||||
|   bool _isReady = false; |   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 { |   Future<void> _initialize() async { | ||||||
|     try { |     try { | ||||||
|       final home = context.read<HomeWidgetProvider>(); |       final home = context.read<HomeWidgetProvider>(); | ||||||
|       await home.initialize(); |       await home.initialize(); | ||||||
|       if (!mounted) return; |       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>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|       await sn.initializeUserAgent(); |       await sn.initializeUserAgent(); | ||||||
|  |       await sn.setConfigWithNative(); | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       final ua = context.read<UserProvider>(); |       final ua = context.read<UserProvider>(); | ||||||
|       await ua.initialize(); |       await ua.initialize(); | ||||||
| @@ -189,10 +287,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<void> _postInitialization() async { | ||||||
|  |     await widgetUpdateRandomPost(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _initialize(); |     _initialize().then((_) { | ||||||
|  |       _postInitialization(); | ||||||
|  |       _tryRequestRating(); | ||||||
|  |       _checkForUpdate(); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -206,7 +312,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | |||||||
|             mainAxisAlignment: MainAxisAlignment.center, |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|             mainAxisSize: MainAxisSize.min, |             mainAxisSize: MainAxisSize.min, | ||||||
|             children: [ |             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), |               const Gap(6), | ||||||
|               LinearProgressIndicator( |               LinearProgressIndicator( | ||||||
|                 backgroundColor: Theme.of(context).colorScheme.surfaceContainer, |                 backgroundColor: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|   | |||||||
| @@ -125,10 +125,8 @@ class ChatChannelProvider extends ChangeNotifier { | |||||||
|       final channelBox = await Hive.openBox<SnChatMessage>( |       final channelBox = await Hive.openBox<SnChatMessage>( | ||||||
|         '${ChatMessageController.kChatMessageBoxPrefix}${channel.id}', |         '${ChatMessageController.kChatMessageBoxPrefix}${channel.id}', | ||||||
|       ); |       ); | ||||||
|       final lastMessage = channelBox.isNotEmpty |       final lastMessage = | ||||||
|           ? channelBox.values |           channelBox.isNotEmpty ? channelBox.values.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) : null; | ||||||
|               .reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) |  | ||||||
|           : null; |  | ||||||
|       if (lastMessage != null) result.add(lastMessage); |       if (lastMessage != null) result.add(lastMessage); | ||||||
|       channelBox.close(); |       channelBox.close(); | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								lib/providers/config.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | |||||||
|  | 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 kAppbarTransparentStoreKey = 'app_bar_transparent'; | ||||||
|  | const kAppBackgroundStoreKey = 'app_has_background'; | ||||||
|  | const kAppColorSchemeStoreKey = 'app_color_scheme'; | ||||||
|  |  | ||||||
|  | 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(); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								lib/providers/experience.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | |||||||
|  | import 'package:intl/intl.dart'; | ||||||
|  |  | ||||||
|  | const List<int> kExperienceToLevelRequirements = [ | ||||||
|  |   0, // Level 0 | ||||||
|  |   1000, // Level 1 | ||||||
|  |   4000, // Level 2 | ||||||
|  |   9000, // Level 3 | ||||||
|  |   16000, // Level 4 | ||||||
|  |   25000, // Level 5 | ||||||
|  |   36000, // Level 6 | ||||||
|  |   49000, // Level 7 | ||||||
|  |   64000, // Level 8 | ||||||
|  |   81000, // Level 9 | ||||||
|  |   100000, // Level 10 | ||||||
|  |   121000, // Level 11 | ||||||
|  |   144000, // Level 12 | ||||||
|  |   368000 // Level 13 | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | int getLevelFromExp(int experience) { | ||||||
|  |   final exp = kExperienceToLevelRequirements.reversed.firstWhere((x) => x <= experience); | ||||||
|  |   final idx = kExperienceToLevelRequirements.indexOf(exp); | ||||||
|  |   return idx; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | double calcLevelUpProgress(int experience) { | ||||||
|  |   final exp = kExperienceToLevelRequirements.reversed.firstWhere((x) => x <= experience); | ||||||
|  |   final idx = kExperienceToLevelRequirements.indexOf(exp); | ||||||
|  |   if (idx + 1 >= kExperienceToLevelRequirements.length) return 1; | ||||||
|  |   final nextExp = kExperienceToLevelRequirements[idx + 1]; | ||||||
|  |   return (experience - exp).abs() / (exp - nextExp).abs(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String calcLevelUpProgressLevel(int experience) { | ||||||
|  |   final exp = kExperienceToLevelRequirements.reversed.firstWhere((x) => x <= experience); | ||||||
|  |   final idx = kExperienceToLevelRequirements.indexOf(exp); | ||||||
|  |   if (idx + 1 >= kExperienceToLevelRequirements.length) return 'Infinity'; | ||||||
|  |   final nextExp = exp - kExperienceToLevelRequirements[idx + 1]; | ||||||
|  |   final formatter = NumberFormat.compactCurrency(symbol: '', decimalDigits: 1); | ||||||
|  |   return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}'; | ||||||
|  | } | ||||||
| @@ -83,12 +83,16 @@ class SnPostContentProvider { | |||||||
|     int offset = 0, |     int offset = 0, | ||||||
|     String? type, |     String? type, | ||||||
|     String? author, |     String? author, | ||||||
|  |     Iterable<String>? categories, | ||||||
|  |     Iterable<String>? tags, | ||||||
|   }) async { |   }) async { | ||||||
|     final resp = await _sn.client.get('/cgi/co/posts', queryParameters: { |     final resp = await _sn.client.get('/cgi/co/posts', queryParameters: { | ||||||
|       'take': take, |       'take': take, | ||||||
|       'offset': offset, |       'offset': offset, | ||||||
|       if (type != null) 'type': type, |       if (type != null) 'type': type, | ||||||
|       if (author != null) 'author': author, |       if (author != null) 'author': author, | ||||||
|  |       if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), | ||||||
|  |       if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','), | ||||||
|     }); |     }); | ||||||
|     final List<SnPost> out = await _preloadRelatedDataInBatch( |     final List<SnPost> out = await _preloadRelatedDataInBatch( | ||||||
|       List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), |       List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), | ||||||
| @@ -118,12 +122,14 @@ class SnPostContentProvider { | |||||||
|     int take = 10, |     int take = 10, | ||||||
|     int offset = 0, |     int offset = 0, | ||||||
|     Iterable<String>? tags, |     Iterable<String>? tags, | ||||||
|  |     Iterable<String>? categories, | ||||||
|   }) async { |   }) async { | ||||||
|     final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: { |     final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: { | ||||||
|       'take': take, |       'take': take, | ||||||
|       'offset': offset, |       'offset': offset, | ||||||
|       'probe': searchTerm, |       'probe': searchTerm, | ||||||
|       if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), |       if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), | ||||||
|  |       if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','), | ||||||
|     }); |     }); | ||||||
|     final List<SnPost> out = await _preloadRelatedDataInBatch( |     final List<SnPost> out = await _preloadRelatedDataInBatch( | ||||||
|       List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), |       List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), | ||||||
|   | |||||||
| @@ -41,8 +41,7 @@ class SnAttachmentProvider { | |||||||
|     return out; |     return out; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<List<SnAttachment?>> getMultiple(List<String> rids, |   Future<List<SnAttachment?>> getMultiple(List<String> rids, {noCache = false}) async { | ||||||
|       {noCache = false}) async { |  | ||||||
|     final result = List<SnAttachment?>.filled(rids.length, null); |     final result = List<SnAttachment?>.filled(rids.length, null); | ||||||
|     final Map<String, int> randomMapping = {}; |     final Map<String, int> randomMapping = {}; | ||||||
|     for (int i = 0; i < rids.length; i++) { |     for (int i = 0; i < rids.length; i++) { | ||||||
| @@ -63,9 +62,7 @@ class SnAttachmentProvider { | |||||||
|           'id': pendingFetch.join(','), |           'id': pendingFetch.join(','), | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|       final out = resp.data['data'] |       final out = resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).toList(); | ||||||
|           .map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)) |  | ||||||
|           .toList(); |  | ||||||
|  |  | ||||||
|       for (final item in out) { |       for (final item in out) { | ||||||
|         if (item == null) continue; |         if (item == null) continue; | ||||||
| @@ -79,10 +76,7 @@ class SnAttachmentProvider { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static Map<String, String> mimetypeOverrides = { |   static Map<String, String> mimetypeOverrides = {'mov': 'video/quicktime', 'mp4': 'video/mp4'}; | ||||||
|     'mov': 'video/quicktime', |  | ||||||
|     'mp4': 'video/mp4' |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   Future<SnAttachment> directUploadOne( |   Future<SnAttachment> directUploadOne( | ||||||
|     Uint8List data, |     Uint8List data, | ||||||
| @@ -93,11 +87,8 @@ class SnAttachmentProvider { | |||||||
|     Function(double progress)? onProgress, |     Function(double progress)? onProgress, | ||||||
|   }) async { |   }) async { | ||||||
|     final filePayload = MultipartFile.fromBytes(data, filename: filename); |     final filePayload = MultipartFile.fromBytes(data, filename: filename); | ||||||
|     final fileAlt = filename.contains('.') |     final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename; | ||||||
|         ? filename.substring(0, filename.lastIndexOf('.')) |     final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); | ||||||
|         : filename; |  | ||||||
|     final fileExt = |  | ||||||
|         filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); |  | ||||||
|  |  | ||||||
|     String? mimetypeOverride; |     String? mimetypeOverride; | ||||||
|     if (mimetype != null) { |     if (mimetype != null) { | ||||||
| @@ -133,11 +124,8 @@ class SnAttachmentProvider { | |||||||
|     Map<String, dynamic>? metadata, { |     Map<String, dynamic>? metadata, { | ||||||
|     String? mimetype, |     String? mimetype, | ||||||
|   }) async { |   }) async { | ||||||
|     final fileAlt = filename.contains('.') |     final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename; | ||||||
|         ? filename.substring(0, filename.lastIndexOf('.')) |     final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); | ||||||
|         : filename; |  | ||||||
|     final fileExt = |  | ||||||
|         filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); |  | ||||||
|  |  | ||||||
|     String? mimetypeOverride; |     String? mimetypeOverride; | ||||||
|     if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) { |     if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) { | ||||||
| @@ -155,10 +143,7 @@ class SnAttachmentProvider { | |||||||
|       if (mimetypeOverride != null) 'mimetype': mimetypeOverride, |       if (mimetypeOverride != null) 'mimetype': mimetypeOverride, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     return ( |     return (SnAttachment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int); | ||||||
|       SnAttachment.fromJson(resp.data['meta']), |  | ||||||
|       resp.data['chunk_size'] as int |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<SnAttachment> _chunkedUploadOnePart( |   Future<SnAttachment> _chunkedUploadOnePart( | ||||||
| @@ -200,24 +185,17 @@ class SnAttachmentProvider { | |||||||
|           (entry.value + 1) * chunkSize, |           (entry.value + 1) * chunkSize, | ||||||
|           await file.length(), |           await file.length(), | ||||||
|         ); |         ); | ||||||
|         final data = Uint8List.fromList(await file |         final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList()); | ||||||
|             .openRead(beginCursor, endCursor) |  | ||||||
|             .expand((chunk) => chunk) |  | ||||||
|             .toList()); |  | ||||||
|  |  | ||||||
|         place = await _chunkedUploadOnePart( |         place = await _chunkedUploadOnePart( | ||||||
|           data, |           data, | ||||||
|           place.rid, |           place.rid, | ||||||
|           entry.key, |           entry.key, | ||||||
|           onProgress: (chunkProgress) { |  | ||||||
|             final overallProgress = |  | ||||||
|                 (currentTask + chunkProgress) / chunks.length; |  | ||||||
|             if (onProgress != null) { |  | ||||||
|               onProgress(overallProgress); |  | ||||||
|             } |  | ||||||
|           }, |  | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         final overallProgress = currentTask / chunks.length; | ||||||
|  |         onProgress?.call(overallProgress); | ||||||
|  |  | ||||||
|         currentTask++; |         currentTask++; | ||||||
|       }()); |       }()); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -6,30 +6,34 @@ import 'dart:io'; | |||||||
| import 'package:dio/dio.dart'; | import 'package:dio/dio.dart'; | ||||||
| import 'package:dio_smart_retry/dio_smart_retry.dart'; | import 'package:dio_smart_retry/dio_smart_retry.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; | import 'package:package_info_plus/package_info_plus.dart'; | ||||||
| import 'package:device_info_plus/device_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:shared_preferences/shared_preferences.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
|  | import 'package:surface/providers/widget.dart'; | ||||||
| import 'package:synchronized/synchronized.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 = [ | const kNetworkServerDirectory = [ | ||||||
|   ('Solar Network', 'https://api.sn.solsynth.dev'), |   ('Solar Network', 'https://api.sn.solsynth.dev'), | ||||||
|   ('Local', 'http://localhost:8001'), |   ('Local', 'http://localhost:8001'), | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | Completer<String?>? _refreshCompleter; | ||||||
|  |  | ||||||
| class SnNetworkProvider { | class SnNetworkProvider { | ||||||
|   late final Dio client; |   late final Dio client; | ||||||
|  |  | ||||||
|   late final SharedPreferences _prefs; |   late final SharedPreferences _prefs; | ||||||
|  |   late final ConfigProvider _config; | ||||||
|  |   late final HomeWidgetProvider _home; | ||||||
|  |  | ||||||
|   String? _userAgent; |   String? _userAgent; | ||||||
|  |  | ||||||
|   SnNetworkProvider() { |   SnNetworkProvider(BuildContext context) { | ||||||
|  |     _home = context.read<HomeWidgetProvider>(); | ||||||
|  |  | ||||||
|     client = Dio(); |     client = Dio(); | ||||||
|  |  | ||||||
|     client.interceptors.add(RetryInterceptor( |     client.interceptors.add(RetryInterceptor( | ||||||
| @@ -60,13 +64,55 @@ class SnNetworkProvider { | |||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     SharedPreferences.getInstance().then((prefs) { |     _config = context.read<ConfigProvider>(); | ||||||
|       _prefs = prefs; |     _config.initialize().then((_) { | ||||||
|       client.options.baseUrl = _prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; |       _prefs = _config.prefs; | ||||||
|  |       client.options.baseUrl = _config.serverUrl; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> setConfigWithNative() async { | ||||||
|  |     _home.saveWidgetData("nex_server_url", client.options.baseUrl); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<String> _getUserAgent() async { | ||||||
|     final String platformInfo; |     final String platformInfo; | ||||||
|     if (kIsWeb) { |     if (kIsWeb) { | ||||||
|       final deviceInfo = await DeviceInfoPlugin().webBrowserInfo; |       final deviceInfo = await DeviceInfoPlugin().webBrowserInfo; | ||||||
| @@ -92,14 +138,22 @@ class SnNetworkProvider { | |||||||
|  |  | ||||||
|     final packageInfo = await PackageInfo.fromPlatform(); |     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(); |   final tkLock = Lock(); | ||||||
|  |  | ||||||
|   Completer<String?>? _refreshCompleter; |  | ||||||
|  |  | ||||||
|   Future<String?> getFreshAtk() async { |   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) { |     if (_refreshCompleter != null) { | ||||||
|       return await _refreshCompleter!.future; |       return await _refreshCompleter!.future; | ||||||
|     } else { |     } else { | ||||||
| @@ -107,7 +161,6 @@ class SnNetworkProvider { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       var atk = _prefs.getString(kAtkStoreKey); |  | ||||||
|       if (atk != null) { |       if (atk != null) { | ||||||
|         final atkParts = atk.split('.'); |         final atkParts = atk.split('.'); | ||||||
|         if (atkParts.length != 3) { |         if (atkParts.length != 3) { | ||||||
| @@ -133,7 +186,13 @@ class SnNetworkProvider { | |||||||
|         final exp = jsonDecode(payload)['exp']; |         final exp = jsonDecode(payload)['exp']; | ||||||
|         if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { |         if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { | ||||||
|           log('Access token need refresh, doing it at ${DateTime.now()}'); |           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) { |         if (atk != null) { | ||||||
| @@ -171,24 +230,32 @@ class SnNetworkProvider { | |||||||
|  |  | ||||||
|   Future<String?> refreshToken() async { |   Future<String?> refreshToken() async { | ||||||
|     final rtk = _prefs.getString(kRtkStoreKey); |     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; |     if (rtk == null) return null; | ||||||
|  |  | ||||||
|     final dio = Dio(); |     final dio = Dio(); | ||||||
|     dio.options.baseUrl = client.options.baseUrl; |     dio.options.baseUrl = baseUrl; | ||||||
|  |  | ||||||
|     final resp = await dio.post('/cgi/id/auth/token', data: { |     final resp = await dio.post('/cgi/id/auth/token', data: { | ||||||
|       'grant_type': 'refresh_token', |       'grant_type': 'refresh_token', | ||||||
|       'refresh_token': rtk, |       'refresh_token': rtk, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     final atk = resp.data['access_token']; |     final String atk = resp.data['access_token']; | ||||||
|     final nRtk = resp.data['refresh_token']; |     final String nRtk = resp.data['refresh_token']; | ||||||
|     setTokenPair(atk, nRtk); |  | ||||||
|  |  | ||||||
|     return atk; |     return (atk, nRtk); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void setBaseUrl(String url) { |   void setBaseUrl(String url) { | ||||||
|  |     _config.serverUrl = url; | ||||||
|     client.options.baseUrl = url; |     client.options.baseUrl = url; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										136
									
								
								lib/providers/special_day.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,136 @@ | |||||||
|  | import 'package:flutter/widgets.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
|  | import 'package:surface/providers/userinfo.dart'; | ||||||
|  |  | ||||||
|  | // Stored as key: month, day | ||||||
|  | const Map<String, (int, int)> kSpecialDays = { | ||||||
|  |   // Birthday is dynamically generated according to the user's profile | ||||||
|  |   'NewYear': (1, 1), | ||||||
|  |   'ValentineDay': (2, 14), | ||||||
|  |   'LaborDay': (5, 1), | ||||||
|  |   'MotherDay': (5, 11), | ||||||
|  |   'ChildrenDay': (6, 1), | ||||||
|  |   'FatherDay': (8, 8), | ||||||
|  |   'Halloween': (10, 31), | ||||||
|  |   'Thanksgiving': (11, 28), | ||||||
|  |   'MerryXmas': (12, 25), | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const Map<String, String> kSpecialDaysSymbol = { | ||||||
|  |   'Birthday': '🎂', | ||||||
|  |   'NewYear': '🎉', | ||||||
|  |   'MerryXmas': '🎄', | ||||||
|  |   'ValentineDay': '💑', | ||||||
|  |   'LaborDay': '🏋️', | ||||||
|  |   'MotherDay': '👩', | ||||||
|  |   'ChildrenDay': '👶', | ||||||
|  |   'FatherDay': '👨', | ||||||
|  |   'Halloween': '🎃', | ||||||
|  |   'Thanksgiving': '🎅', | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class SpecialDayProvider { | ||||||
|  |   late final UserProvider _user; | ||||||
|  |  | ||||||
|  |   SpecialDayProvider(BuildContext context) { | ||||||
|  |     _user = context.read<UserProvider>(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   List<String> getSpecialDays() { | ||||||
|  |     final now = DateTime.now().toLocal(); | ||||||
|  |     final birthday = _user.user?.profile?.birthday?.toLocal(); | ||||||
|  |     final isBirthday = birthday != null && birthday.day == now.day && birthday.month == now.month; | ||||||
|  |  | ||||||
|  |     return [ | ||||||
|  |       if (isBirthday) 'Birthday', | ||||||
|  |       ...kSpecialDays.keys.where( | ||||||
|  |         (key) => kSpecialDays[key]!.$1 == now.month && kSpecialDays[key]!.$2 == now.day, | ||||||
|  |       ), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   (String, DateTime)? getLastSpecialDay() { | ||||||
|  |     final now = DateTime.now().toLocal(); | ||||||
|  |     final birthday = _user.user?.profile?.birthday?.toLocal(); | ||||||
|  |  | ||||||
|  |     final Map<String, (int, int)> specialDays = { | ||||||
|  |       if (birthday != null) 'Birthday': (birthday.month, birthday.day), | ||||||
|  |       ...kSpecialDays, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     DateTime? lastDate; | ||||||
|  |     String? lastEvent; | ||||||
|  |  | ||||||
|  |     for (final entry in specialDays.entries) { | ||||||
|  |       final eventName = entry.key; | ||||||
|  |       final (month, day) = entry.value; | ||||||
|  |  | ||||||
|  |       var specialDayThisYear = DateTime(now.year, month, day); | ||||||
|  |       var specialDayLastYear = DateTime(now.year - 1, month, day); | ||||||
|  |  | ||||||
|  |       if (specialDayThisYear.isBefore(now)) { | ||||||
|  |         if (lastDate == null || specialDayThisYear.isAfter(lastDate)) { | ||||||
|  |           lastDate = specialDayThisYear; | ||||||
|  |           lastEvent = eventName; | ||||||
|  |         } | ||||||
|  |       } else if (specialDayLastYear.isBefore(now)) { | ||||||
|  |         if (lastDate == null || specialDayLastYear.isAfter(lastDate)) { | ||||||
|  |           lastDate = specialDayLastYear; | ||||||
|  |           lastEvent = eventName; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (lastEvent != null && lastDate != null) { | ||||||
|  |       return (lastEvent, lastDate); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   (String, DateTime)? getNextSpecialDay() { | ||||||
|  |     final now = DateTime.now().toLocal(); | ||||||
|  |     final birthday = _user.user?.profile?.birthday?.toLocal(); | ||||||
|  |  | ||||||
|  |     // Stored as key: month, day | ||||||
|  |     final Map<String, (int, int)> specialDays = { | ||||||
|  |       if (birthday != null) 'Birthday': (birthday.month, birthday.day), | ||||||
|  |       ...kSpecialDays, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     DateTime? closestDate; | ||||||
|  |     String? closestEvent; | ||||||
|  |  | ||||||
|  |     for (final entry in specialDays.entries) { | ||||||
|  |       final eventName = entry.key; | ||||||
|  |       final (month, day) = entry.value; | ||||||
|  |  | ||||||
|  |       // Calculate the special day's DateTime in the current year | ||||||
|  |       var specialDay = DateTime(now.year, month, day); | ||||||
|  |  | ||||||
|  |       // If the special day has already passed this year, consider it for the next year | ||||||
|  |       if (specialDay.isBefore(now)) { | ||||||
|  |         specialDay = DateTime(now.year + 1, month, day); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Check if this special day is closer than the previously found one | ||||||
|  |       if (closestDate == null || specialDay.isBefore(closestDate)) { | ||||||
|  |         closestDate = specialDay; | ||||||
|  |         closestEvent = eventName; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (closestEvent != null && closestDate != null) { | ||||||
|  |       return (closestEvent, closestDate); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // No special day found | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   double getSpecialDayProgress(DateTime last, DateTime next) { | ||||||
|  |     final totalDuration = next.difference(last).inSeconds.toDouble(); | ||||||
|  |     final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble(); | ||||||
|  |     return (elapsedDuration / totalDuration).clamp(0.0, 1.0); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import 'dart:ui'; | ||||||
|  |  | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:surface/theme.dart'; | import 'package:surface/theme.dart'; | ||||||
|  |  | ||||||
| @@ -11,8 +13,8 @@ class ThemeProvider extends ChangeNotifier { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void reloadTheme({bool? useMaterial3}) { |   void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) { | ||||||
|     createAppThemeSet().then((value) { |     createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) { | ||||||
|       theme = value; |       theme = value; | ||||||
|       notifyListeners(); |       notifyListeners(); | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| import 'dart:developer'; | import 'dart:developer'; | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:home_widget/home_widget.dart'; |  | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.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/sn_network.dart'; | ||||||
| import 'package:surface/providers/widget.dart'; | import 'package:surface/providers/widget.dart'; | ||||||
| import 'package:surface/types/account.dart'; | import 'package:surface/types/account.dart'; | ||||||
| @@ -14,10 +14,12 @@ class UserProvider extends ChangeNotifier { | |||||||
|  |  | ||||||
|   late final SnNetworkProvider _sn; |   late final SnNetworkProvider _sn; | ||||||
|   late final HomeWidgetProvider _home; |   late final HomeWidgetProvider _home; | ||||||
|  |   late final ConfigProvider _config; | ||||||
|  |  | ||||||
|   UserProvider(BuildContext context) { |   UserProvider(BuildContext context) { | ||||||
|     _sn = context.read<SnNetworkProvider>(); |     _sn = context.read<SnNetworkProvider>(); | ||||||
|     _home = context.read<HomeWidgetProvider>(); |     _home = context.read<HomeWidgetProvider>(); | ||||||
|  |     _config = context.read<ConfigProvider>(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String?> get atk async { |   Future<String?> get atk async { | ||||||
| @@ -26,14 +28,12 @@ class UserProvider extends ChangeNotifier { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> initialize() async { |   Future<void> initialize() async { | ||||||
|     final prefs = await SharedPreferences.getInstance(); |     final value = _config.prefs.getString(kAtkStoreKey); | ||||||
|     final value = prefs.getString(kAtkStoreKey); |  | ||||||
|     isAuthorized = value != null; |     isAuthorized = value != null; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|     refreshUser().then((value) { |     refreshUser().then((value) { | ||||||
|       if (value != null) { |       if (value != null) { | ||||||
|         log('Logged in as @${value.name}'); |         log('Logged in as @${value.name}'); | ||||||
|         _home.saveWidgetData('user', value.toJson()); |  | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,22 +1,24 @@ | |||||||
|  | import 'dart:async'; | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:home_widget/home_widget.dart'; | import 'package:home_widget/home_widget.dart'; | ||||||
|  | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/types/post.dart'; | ||||||
|  |  | ||||||
| class HomeWidgetProvider { | class HomeWidgetProvider { | ||||||
|   HomeWidgetProvider(BuildContext context); |   HomeWidgetProvider(BuildContext context); | ||||||
|  |  | ||||||
|   Future<void> initialize() async { |   Future<void> initialize() async { | ||||||
|     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; |     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; | ||||||
|     if (!kIsWeb && Platform.isIOS) { |     if (Platform.isIOS) { | ||||||
|       await HomeWidget.setAppGroupId("group.solsynth.solian"); |       await HomeWidget.setAppGroupId("group.solsynth.solian"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> saveWidgetData(String id, dynamic data, |   Future<void> saveWidgetData(String id, dynamic data, {bool update = true}) async { | ||||||
|       {bool update = true}) async { |  | ||||||
|     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; |     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; | ||||||
|     await HomeWidget.saveWidgetData(id, jsonEncode(data)); |     await HomeWidget.saveWidgetData(id, jsonEncode(data)); | ||||||
|     if (update) await updateWidget(); |     if (update) await updateWidget(); | ||||||
| @@ -25,7 +27,7 @@ class HomeWidgetProvider { | |||||||
|   Future<void> updateWidget() async { |   Future<void> updateWidget() async { | ||||||
|     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; |     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; | ||||||
|     if (Platform.isIOS) { |     if (Platform.isIOS) { | ||||||
|       const widgets = ["SolarFeaturedPostWidget", "SolarCheckInWidget"]; |       const widgets = ["SolarRandomPostWidget", "SolarCheckInWidget"]; | ||||||
|       for (final widget in widgets) { |       for (final widget in widgets) { | ||||||
|         await HomeWidget.updateWidget( |         await HomeWidget.updateWidget( | ||||||
|           name: widget, |           name: widget, | ||||||
| @@ -33,7 +35,7 @@ class HomeWidgetProvider { | |||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|     } else if (Platform.isAndroid) { |     } else if (Platform.isAndroid) { | ||||||
|       const widgets = ["FeaturedPostWidget", "CheckInWidget"]; |       const widgets = ["RandomPostWidget", "CheckInWidget"]; | ||||||
|       for (final widget in widgets) { |       for (final widget in widgets) { | ||||||
|         await HomeWidget.updateWidget( |         await HomeWidget.updateWidget( | ||||||
|           androidName: "${widget}Receiver", |           androidName: "${widget}Receiver", | ||||||
| @@ -43,3 +45,16 @@ class HomeWidgetProvider { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | Future<void> widgetUpdateRandomPost() async { | ||||||
|  |   final snc = await SnNetworkProvider.createOffContextClient(); | ||||||
|  |   final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1'); | ||||||
|  |   final post = SnPost.fromJson(resp.data['data'][0]); | ||||||
|  |   await HomeWidget.saveWidgetData("int_random_post", jsonEncode(post.toJson())); | ||||||
|  |   await HomeWidget.updateWidget( | ||||||
|  |     name: "SolarRandomPostWidget", | ||||||
|  |     iOSName: "SolarRandomPostWidget", | ||||||
|  |     androidName: "RandomPostWidgetReceiver", | ||||||
|  |     qualifiedAndroidName: "dev.solsynth.solian.widgets.RandomPostWidgetReceiver", | ||||||
|  |   ); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -77,8 +77,11 @@ final _appRoutes = [ | |||||||
|           GoRoute( |           GoRoute( | ||||||
|             path: '/search', |             path: '/search', | ||||||
|             name: 'postSearch', |             name: 'postSearch', | ||||||
|             builder: (context, state) => const AppBackground( |             builder: (context, state) => AppBackground( | ||||||
|               child: PostSearchScreen(), |               child: PostSearchScreen( | ||||||
|  |                 initialTags: state.uri.queryParameters['tags']?.split(','), | ||||||
|  |                 initialCategories: state.uri.queryParameters['categories']?.split(','), | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           GoRoute( |           GoRoute( | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import 'dart:ui'; | import 'dart:ui'; | ||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:fl_chart/fl_chart.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| @@ -9,10 +10,12 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:relative_time/relative_time.dart'; | import 'package:relative_time/relative_time.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/providers/experience.dart'; | ||||||
| import 'package:surface/providers/relationship.dart'; | import 'package:surface/providers/relationship.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/screens/abuse_report.dart'; | import 'package:surface/screens/abuse_report.dart'; | ||||||
| import 'package:surface/types/account.dart'; | import 'package:surface/types/account.dart'; | ||||||
|  | import 'package:surface/types/check_in.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| @@ -61,6 +64,19 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<List<SnCheckInRecord>> _getCheckInRecords() async { | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14'); | ||||||
|  |       return List.from( | ||||||
|  |         resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [], | ||||||
|  |       ); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (mounted) context.showErrorDialog(err); | ||||||
|  |       rethrow; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   SnAccountStatusInfo? _status; |   SnAccountStatusInfo? _status; | ||||||
|  |  | ||||||
|   Future<void> _fetchStatus() async { |   Future<void> _fetchStatus() async { | ||||||
| @@ -228,65 +244,72 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|       body: CustomScrollView( |       body: CustomScrollView( | ||||||
|         controller: _scrollController, |         controller: _scrollController, | ||||||
|         slivers: [ |         slivers: [ | ||||||
|           SliverAppBar( |           Theme( | ||||||
|             expandedHeight: _appBarHeight, |             data: Theme.of(context).copyWith( | ||||||
|             title: _account == null |               appBarTheme: Theme.of(context).appBarTheme.copyWith( | ||||||
|                 ? Text('loading').tr() |                     foregroundColor: Colors.white, | ||||||
|                 : RichText( |  | ||||||
|                     textAlign: TextAlign.center, |  | ||||||
|                     text: TextSpan(children: [ |  | ||||||
|                       TextSpan( |  | ||||||
|                         text: _account!.nick, |  | ||||||
|                         style: Theme.of(context).textTheme.titleLarge!.copyWith( |  | ||||||
|                               color: Theme.of(context).appBarTheme.foregroundColor!, |  | ||||||
|                               shadows: labelShadows, |  | ||||||
|                             ), |  | ||||||
|                       ), |  | ||||||
|                       const TextSpan(text: '\n'), |  | ||||||
|                       TextSpan( |  | ||||||
|                         text: '@${_account!.name}', |  | ||||||
|                         style: Theme.of(context).textTheme.bodySmall!.copyWith( |  | ||||||
|                               color: Theme.of(context).appBarTheme.foregroundColor!, |  | ||||||
|                               shadows: labelShadows, |  | ||||||
|                             ), |  | ||||||
|                       ), |  | ||||||
|                     ]), |  | ||||||
|                   ), |                   ), | ||||||
|             pinned: true, |             ), | ||||||
|             flexibleSpace: _account != null |             child: SliverAppBar( | ||||||
|                 ? Stack( |               expandedHeight: _appBarHeight, | ||||||
|                     fit: StackFit.expand, |               title: _account == null | ||||||
|                     children: [ |                   ? Text('loading').tr() | ||||||
|                       UniversalImage( |                   : RichText( | ||||||
|                         sn.getAttachmentUrl(_account!.banner), |                       textAlign: TextAlign.center, | ||||||
|                         fit: BoxFit.cover, |                       text: TextSpan(children: [ | ||||||
|                         height: imageHeight, |                         TextSpan( | ||||||
|                         width: _appBarWidth, |                           text: _account!.nick, | ||||||
|                         cacheHeight: imageHeight, |                           style: Theme.of(context).textTheme.titleLarge!.copyWith( | ||||||
|                         cacheWidth: _appBarWidth, |                                 color: Colors.white, | ||||||
|                       ), |                                 shadows: labelShadows, | ||||||
|                       Positioned( |                               ), | ||||||
|                         top: 0, |                         ), | ||||||
|                         left: 0, |                         const TextSpan(text: '\n'), | ||||||
|                         right: 0, |                         TextSpan( | ||||||
|                         height: 56 + MediaQuery.of(context).padding.top, |                           text: '@${_account!.name}', | ||||||
|                         child: ClipRect( |                           style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||||
|                           child: BackdropFilter( |                                 color: Colors.white, | ||||||
|                             filter: ImageFilter.blur( |                                 shadows: labelShadows, | ||||||
|                               sigmaX: _appBarBlur, |                               ), | ||||||
|                               sigmaY: _appBarBlur, |                         ), | ||||||
|                             ), |                       ]), | ||||||
|                             child: Container( |                     ), | ||||||
|                               color: Colors.black.withOpacity( |               pinned: true, | ||||||
|                                 clampDouble(_appBarBlur * 0.1, 0, 0.5), |               flexibleSpace: _account != null | ||||||
|  |                   ? Stack( | ||||||
|  |                       fit: StackFit.expand, | ||||||
|  |                       children: [ | ||||||
|  |                         UniversalImage( | ||||||
|  |                           sn.getAttachmentUrl(_account!.banner), | ||||||
|  |                           fit: BoxFit.cover, | ||||||
|  |                           height: imageHeight, | ||||||
|  |                           width: _appBarWidth, | ||||||
|  |                           cacheHeight: imageHeight, | ||||||
|  |                           cacheWidth: _appBarWidth, | ||||||
|  |                         ), | ||||||
|  |                         Positioned( | ||||||
|  |                           top: 0, | ||||||
|  |                           left: 0, | ||||||
|  |                           right: 0, | ||||||
|  |                           height: 56 + MediaQuery.of(context).padding.top, | ||||||
|  |                           child: ClipRect( | ||||||
|  |                             child: BackdropFilter( | ||||||
|  |                               filter: ImageFilter.blur( | ||||||
|  |                                 sigmaX: _appBarBlur, | ||||||
|  |                                 sigmaY: _appBarBlur, | ||||||
|  |                               ), | ||||||
|  |                               child: Container( | ||||||
|  |                                 color: Colors.black.withOpacity( | ||||||
|  |                                   clampDouble(_appBarBlur * 0.1, 0, 0.5), | ||||||
|  |                                 ), | ||||||
|                               ), |                               ), | ||||||
|                             ), |                             ), | ||||||
|                           ), |                           ), | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ], | ||||||
|                     ], |                     ) | ||||||
|                   ) |                   : null, | ||||||
|                 : null, |             ), | ||||||
|           ), |           ), | ||||||
|           if (_account != null) |           if (_account != null) | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
| @@ -430,6 +453,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|                   Column( |                   Column( | ||||||
|                     children: [ |                     children: [ | ||||||
|                       Row( |                       Row( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                         children: [ |                         children: [ | ||||||
|                           const Icon(Symbols.calendar_add_on), |                           const Icon(Symbols.calendar_add_on), | ||||||
|                           const Gap(8), |                           const Gap(8), | ||||||
| @@ -437,6 +461,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|                         ], |                         ], | ||||||
|                       ), |                       ), | ||||||
|                       Row( |                       Row( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                         children: [ |                         children: [ | ||||||
|                           const Icon(Symbols.cake), |                           const Icon(Symbols.cake), | ||||||
|                           const Gap(8), |                           const Gap(8), | ||||||
| @@ -450,6 +475,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|                         ], |                         ], | ||||||
|                       ), |                       ), | ||||||
|                       Row( |                       Row( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                         children: [ |                         children: [ | ||||||
|                           const Icon(Symbols.identity_platform), |                           const Icon(Symbols.identity_platform), | ||||||
|                           const Gap(8), |                           const Gap(8), | ||||||
| @@ -459,6 +485,26 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|                           ).opacity(0.8), |                           ).opacity(0.8), | ||||||
|                         ], |                         ], | ||||||
|                       ), |                       ), | ||||||
|  |                       Row( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                         children: [ | ||||||
|  |                           const Icon(Symbols.star), | ||||||
|  |                           const Gap(8), | ||||||
|  |                           Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'), | ||||||
|  |                           const Gap(8), | ||||||
|  |                           Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5), | ||||||
|  |                           const Gap(8), | ||||||
|  |                           Container( | ||||||
|  |                             width: double.infinity, | ||||||
|  |                             constraints: const BoxConstraints(maxWidth: 160), | ||||||
|  |                             child: LinearProgressIndicator( | ||||||
|  |                               value: calcLevelUpProgress(_account?.profile?.experience ?? 0), | ||||||
|  |                               borderRadius: BorderRadius.circular(8), | ||||||
|  |                               backgroundColor: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|  |                             ).alignment(Alignment.centerLeft), | ||||||
|  |                           ), | ||||||
|  |                         ], | ||||||
|  |                       ), | ||||||
|                     ], |                     ], | ||||||
|                   ).padding(horizontal: 8), |                   ).padding(horizontal: 8), | ||||||
|                 ], |                 ], | ||||||
| @@ -466,6 +512,27 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|             ), |             ), | ||||||
|           SliverToBoxAdapter(child: const Divider()), |           SliverToBoxAdapter(child: const Divider()), | ||||||
|           const SliverGap(12), |           const SliverGap(12), | ||||||
|  |           SliverToBoxAdapter( | ||||||
|  |             child: FutureBuilder<List<SnCheckInRecord>>( | ||||||
|  |               future: _getCheckInRecords(), | ||||||
|  |               builder: (context, snapshot) { | ||||||
|  |                 if (!snapshot.hasData) return const SizedBox.shrink(); | ||||||
|  |                 final records = snapshot.data!; | ||||||
|  |                 return SizedBox( | ||||||
|  |                   width: double.infinity, | ||||||
|  |                   height: 240, | ||||||
|  |                   child: CheckInRecordChart(records: records), | ||||||
|  |                 ).padding( | ||||||
|  |                   right: 24, | ||||||
|  |                   left: 16, | ||||||
|  |                   top: 12, | ||||||
|  |                 ); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           const SliverGap(12), | ||||||
|  |           SliverToBoxAdapter(child: const Divider()), | ||||||
|  |           const SliverGap(12), | ||||||
|           SliverToBoxAdapter( |           SliverToBoxAdapter( | ||||||
|             child: Column( |             child: Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
| @@ -534,3 +601,105 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class CheckInRecordChart extends StatelessWidget { | ||||||
|  |   const CheckInRecordChart({ | ||||||
|  |     super.key, | ||||||
|  |     required this.records, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   final List<SnCheckInRecord> records; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return LineChart( | ||||||
|  |       LineChartData( | ||||||
|  |         lineBarsData: [ | ||||||
|  |           LineChartBarData( | ||||||
|  |             color: Theme.of(context).colorScheme.primary, | ||||||
|  |             belowBarData: BarAreaData( | ||||||
|  |               show: true, | ||||||
|  |               gradient: LinearGradient( | ||||||
|  |                 colors: List.filled( | ||||||
|  |                   records.length, | ||||||
|  |                   Theme.of(context).colorScheme.primary.withOpacity(0.3), | ||||||
|  |                 ).toList(), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             spots: records | ||||||
|  |                 .map( | ||||||
|  |                   (x) => FlSpot( | ||||||
|  |                     x.createdAt | ||||||
|  |                         .copyWith( | ||||||
|  |                           hour: 0, | ||||||
|  |                           minute: 0, | ||||||
|  |                           second: 0, | ||||||
|  |                           millisecond: 0, | ||||||
|  |                           microsecond: 0, | ||||||
|  |                         ) | ||||||
|  |                         .millisecondsSinceEpoch | ||||||
|  |                         .toDouble(), | ||||||
|  |                     x.resultTier.toDouble(), | ||||||
|  |                   ), | ||||||
|  |                 ) | ||||||
|  |                 .toList(), | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |         lineTouchData: LineTouchData( | ||||||
|  |           touchTooltipData: LineTouchTooltipData( | ||||||
|  |             getTooltipItems: (spots) => spots | ||||||
|  |                 .map( | ||||||
|  |                   (spot) => LineTooltipItem( | ||||||
|  |                     '${kCheckInResultTierSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}', | ||||||
|  |                     TextStyle( | ||||||
|  |                       color: Theme.of(context).colorScheme.onSurface, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ) | ||||||
|  |                 .toList(), | ||||||
|  |             getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         titlesData: FlTitlesData( | ||||||
|  |           topTitles: const AxisTitles( | ||||||
|  |             sideTitles: SideTitles(showTitles: false), | ||||||
|  |           ), | ||||||
|  |           rightTitles: const AxisTitles( | ||||||
|  |             sideTitles: SideTitles(showTitles: false), | ||||||
|  |           ), | ||||||
|  |           leftTitles: AxisTitles( | ||||||
|  |             sideTitles: SideTitles( | ||||||
|  |               showTitles: true, | ||||||
|  |               reservedSize: 40, | ||||||
|  |               interval: 1, | ||||||
|  |               getTitlesWidget: (value, _) => Align( | ||||||
|  |                 alignment: Alignment.centerRight, | ||||||
|  |                 child: Text( | ||||||
|  |                   kCheckInResultTierSymbols[value.toInt()], | ||||||
|  |                   textAlign: TextAlign.right, | ||||||
|  |                 ).padding(right: 8), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           bottomTitles: AxisTitles( | ||||||
|  |             sideTitles: SideTitles( | ||||||
|  |               showTitles: true, | ||||||
|  |               reservedSize: 28, | ||||||
|  |               interval: 86400000, | ||||||
|  |               getTitlesWidget: (value, _) => Text( | ||||||
|  |                 DateFormat('dd').format( | ||||||
|  |                   DateTime.fromMillisecondsSinceEpoch( | ||||||
|  |                     value.toInt(), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 textAlign: TextAlign.center, | ||||||
|  |               ).padding(top: 8), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         gridData: const FlGridData(show: false), | ||||||
|  |         borderData: FlBorderData(show: false), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -158,7 +158,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | |||||||
|     GoRouter.of(context).pushNamed( |     GoRouter.of(context).pushNamed( | ||||||
|       'chatCallRoom', |       'chatCallRoom', | ||||||
|       pathParameters: { |       pathParameters: { | ||||||
|         'scope': _channel!.realm!.alias, |         'scope': _channel!.realm?.alias ?? 'global', | ||||||
|         'alias': _channel!.alias, |         'alias': _channel!.alias, | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -5,12 +5,28 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/providers/post.dart'; | import 'package:surface/providers/post.dart'; | ||||||
|  | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/app_bar_leading.dart'; | import 'package:surface/widgets/app_bar_leading.dart'; | ||||||
|  | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/post/post_item.dart'; | import 'package:surface/widgets/post/post_item.dart'; | ||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||||
|  |  | ||||||
|  | const Map<String, IconData> kCategoryIcons = { | ||||||
|  |   'technology': Symbols.tools_wrench, | ||||||
|  |   'gaming': Symbols.gamepad, | ||||||
|  |   'life': Symbols.nightlife, | ||||||
|  |   'arts': Symbols.format_paint, | ||||||
|  |   'sports': Symbols.sports_soccer, | ||||||
|  |   'music': Symbols.music_note, | ||||||
|  |   'news': Symbols.newspaper, | ||||||
|  |   'knowledge': Symbols.library_books, | ||||||
|  |   'literature': Symbols.book, | ||||||
|  |   'funny': Symbols.attractions, | ||||||
|  | }; | ||||||
|  |  | ||||||
| class ExploreScreen extends StatefulWidget { | class ExploreScreen extends StatefulWidget { | ||||||
|   const ExploreScreen({super.key}); |   const ExploreScreen({super.key}); | ||||||
|  |  | ||||||
| @@ -24,15 +40,34 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|   bool _isBusy = true; |   bool _isBusy = true; | ||||||
|  |  | ||||||
|   final List<SnPost> _posts = List.empty(growable: true); |   final List<SnPost> _posts = List.empty(growable: true); | ||||||
|  |   final List<SnPostCategory> _categories = List.empty(growable: true); | ||||||
|   int? _postCount; |   int? _postCount; | ||||||
|  |  | ||||||
|  |   String? _selectedCategory; | ||||||
|  |  | ||||||
|  |   Future<void> _fetchCategories() async { | ||||||
|  |     _categories.clear(); | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final resp = await sn.client.get('/cgi/co/categories?take=100'); | ||||||
|  |       _categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<void> _fetchPosts() async { |   Future<void> _fetchPosts() async { | ||||||
|     if (_postCount != null && _posts.length >= _postCount!) return; |     if (_postCount != null && _posts.length >= _postCount!) return; | ||||||
|  |  | ||||||
|     setState(() => _isBusy = true); |     setState(() => _isBusy = true); | ||||||
|  |  | ||||||
|     final pt = context.read<SnPostContentProvider>(); |     final pt = context.read<SnPostContentProvider>(); | ||||||
|     final result = await pt.listPosts(take: 10, offset: _posts.length); |     final result = await pt.listPosts( | ||||||
|  |       take: 10, | ||||||
|  |       offset: _posts.length, | ||||||
|  |       categories: _selectedCategory != null ? [_selectedCategory!] : null, | ||||||
|  |     ); | ||||||
|     final out = result.$1; |     final out = result.$1; | ||||||
|  |  | ||||||
|     if (!mounted) return; |     if (!mounted) return; | ||||||
| @@ -43,10 +78,17 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|     if (mounted) setState(() => _isBusy = false); |     if (mounted) setState(() => _isBusy = false); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<void> _refreshPosts() { | ||||||
|  |     _postCount = null; | ||||||
|  |     _posts.clear(); | ||||||
|  |     return _fetchPosts(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _fetchPosts(); |     _fetchPosts(); | ||||||
|  |     _fetchCategories(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -59,27 +101,20 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|         type: ExpandableFabType.up, |         type: ExpandableFabType.up, | ||||||
|         childrenAnimation: ExpandableFabAnimation.none, |         childrenAnimation: ExpandableFabAnimation.none, | ||||||
|         overlayStyle: ExpandableFabOverlayStyle( |         overlayStyle: ExpandableFabOverlayStyle( | ||||||
|           color: Theme.of(context) |           color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()), | ||||||
|               .colorScheme |  | ||||||
|               .surface |  | ||||||
|               .withAlpha((255 * 0.5).round()), |  | ||||||
|         ), |         ), | ||||||
|         openButtonBuilder: RotateFloatingActionButtonBuilder( |         openButtonBuilder: RotateFloatingActionButtonBuilder( | ||||||
|           child: const Icon(Symbols.add, size: 28), |           child: const Icon(Symbols.add, size: 28), | ||||||
|           fabSize: ExpandableFabSize.regular, |           fabSize: ExpandableFabSize.regular, | ||||||
|           foregroundColor: |           foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor, | ||||||
|               Theme.of(context).floatingActionButtonTheme.foregroundColor, |           backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor, | ||||||
|           backgroundColor: |  | ||||||
|               Theme.of(context).floatingActionButtonTheme.backgroundColor, |  | ||||||
|           shape: const CircleBorder(), |           shape: const CircleBorder(), | ||||||
|         ), |         ), | ||||||
|         closeButtonBuilder: DefaultFloatingActionButtonBuilder( |         closeButtonBuilder: DefaultFloatingActionButtonBuilder( | ||||||
|           child: const Icon(Symbols.close, size: 28), |           child: const Icon(Symbols.close, size: 28), | ||||||
|           fabSize: ExpandableFabSize.regular, |           fabSize: ExpandableFabSize.regular, | ||||||
|           foregroundColor: |           foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor, | ||||||
|               Theme.of(context).floatingActionButtonTheme.foregroundColor, |           backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor, | ||||||
|           backgroundColor: |  | ||||||
|               Theme.of(context).floatingActionButtonTheme.backgroundColor, |  | ||||||
|           shape: const CircleBorder(), |           shape: const CircleBorder(), | ||||||
|         ), |         ), | ||||||
|         children: [ |         children: [ | ||||||
| @@ -95,8 +130,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|                     'mode': 'stories', |                     'mode': 'stories', | ||||||
|                   }).then((value) { |                   }).then((value) { | ||||||
|                     if (value == true) { |                     if (value == true) { | ||||||
|                       _posts.clear(); |                       _refreshPosts(); | ||||||
|                       _fetchPosts(); |  | ||||||
|                     } |                     } | ||||||
|                   }); |                   }); | ||||||
|                   _fabKey.currentState!.toggle(); |                   _fabKey.currentState!.toggle(); | ||||||
| @@ -117,8 +151,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|                     'mode': 'articles', |                     'mode': 'articles', | ||||||
|                   }).then((value) { |                   }).then((value) { | ||||||
|                     if (value == true) { |                     if (value == true) { | ||||||
|                       _posts.clear(); |                       _refreshPosts(); | ||||||
|                       _fetchPosts(); |  | ||||||
|                     } |                     } | ||||||
|                   }); |                   }); | ||||||
|                   _fabKey.currentState!.toggle(); |                   _fabKey.currentState!.toggle(); | ||||||
| @@ -131,10 +164,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|       ), |       ), | ||||||
|       body: RefreshIndicator( |       body: RefreshIndicator( | ||||||
|         displacement: 40 + MediaQuery.of(context).padding.top, |         displacement: 40 + MediaQuery.of(context).padding.top, | ||||||
|         onRefresh: () { |         onRefresh: () => _refreshPosts(), | ||||||
|           _posts.clear(); |  | ||||||
|           return _fetchPosts(); |  | ||||||
|         }, |  | ||||||
|         child: CustomScrollView( |         child: CustomScrollView( | ||||||
|           slivers: [ |           slivers: [ | ||||||
|             SliverAppBar( |             SliverAppBar( | ||||||
| @@ -151,6 +181,34 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|                 ), |                 ), | ||||||
|                 const Gap(8), |                 const Gap(8), | ||||||
|               ], |               ], | ||||||
|  |               bottom: PreferredSize( | ||||||
|  |                 preferredSize: const Size.fromHeight(50), | ||||||
|  |                 child: SizedBox( | ||||||
|  |                   height: 50, | ||||||
|  |                   child: SingleChildScrollView( | ||||||
|  |                     scrollDirection: Axis.horizontal, | ||||||
|  |                     padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12), | ||||||
|  |                     child: Row( | ||||||
|  |                       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |                       children: _categories.map((ele) { | ||||||
|  |                         return StyledWidget(ChoiceChip( | ||||||
|  |                           avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark), | ||||||
|  |                           label: Text( | ||||||
|  |                             'postCategory${ele.alias.capitalize()}'.trExists() | ||||||
|  |                                 ? 'postCategory${ele.alias.capitalize()}'.tr() | ||||||
|  |                                 : ele.name, | ||||||
|  |                           ), | ||||||
|  |                           selected: _selectedCategory == ele.alias, | ||||||
|  |                           onSelected: (value) { | ||||||
|  |                             _selectedCategory = value ? ele.alias : null; | ||||||
|  |                             _refreshPosts(); | ||||||
|  |                           }, | ||||||
|  |                         )).padding(horizontal: 4); | ||||||
|  |                       }).toList(), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|             SliverInfiniteList( |             SliverInfiniteList( | ||||||
|               itemCount: _posts.length, |               itemCount: _posts.length, | ||||||
| @@ -167,8 +225,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|                       setState(() => _posts[idx] = data); |                       setState(() => _posts[idx] = data); | ||||||
|                     }, |                     }, | ||||||
|                     onDeleted: () { |                     onDeleted: () { | ||||||
|                       _posts.clear(); |                       _refreshPosts(); | ||||||
|                       _fetchPosts(); |  | ||||||
|                     }, |                     }, | ||||||
|                   ), |                   ), | ||||||
|                   onTap: () { |                   onTap: () { | ||||||
|   | |||||||
| @@ -1,26 +1,31 @@ | |||||||
|  | import 'dart:io'; | ||||||
| import 'dart:math' as math; | import 'dart:math' as math; | ||||||
| import 'dart:ui'; | import 'dart:ui'; | ||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | 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:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:google_fonts/google_fonts.dart'; | import 'package:google_fonts/google_fonts.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
|  | import 'package:relative_time/relative_time.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
| import 'package:surface/providers/post.dart'; | import 'package:surface/providers/post.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/providers/special_day.dart'; | ||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
|  | import 'package:surface/providers/widget.dart'; | ||||||
| import 'package:surface/types/check_in.dart'; | import 'package:surface/types/check_in.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/app_bar_leading.dart'; | import 'package:surface/widgets/app_bar_leading.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/post/post_item.dart'; | import 'package:surface/widgets/post/post_item.dart'; | ||||||
|  |  | ||||||
| import '../providers/widget.dart'; |  | ||||||
|  |  | ||||||
| class HomeScreenDashEntry { | class HomeScreenDashEntry { | ||||||
|   final String name; |   final String name; | ||||||
|   final Widget child; |   final Widget child; | ||||||
| @@ -76,7 +81,8 @@ class _HomeScreenState extends State<HomeScreen> { | |||||||
|                 child: Column( |                 child: Column( | ||||||
|                   mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start, |                   mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start, | ||||||
|                   children: [ |                   children: [ | ||||||
|                     _HomeDashSpecialDayWidget().padding(top: 8, horizontal: 8), |                     _HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)), | ||||||
|  |                     _HomeDashSpecialDayWidget().padding(horizontal: 8), | ||||||
|                     StaggeredGrid.extent( |                     StaggeredGrid.extent( | ||||||
|                       maxCrossAxisExtent: 280, |                       maxCrossAxisExtent: 280, | ||||||
|                       mainAxisSpacing: 8, |                       mainAxisSpacing: 8, | ||||||
| @@ -100,26 +106,111 @@ 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_launcher', | ||||||
|  |                             'https://apps.apple.com/us/app/solian/id6499032345', | ||||||
|  |                           ); | ||||||
|  |                           AzhonAppUpdate.update(model); | ||||||
|  |                           context.showSnackbar('updateOngoing'.tr()); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return SizedBox.shrink(); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| class _HomeDashSpecialDayWidget extends StatelessWidget { | class _HomeDashSpecialDayWidget extends StatelessWidget { | ||||||
|   const _HomeDashSpecialDayWidget({super.key}); |   const _HomeDashSpecialDayWidget({super.key}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final ua = context.watch<UserProvider>(); |     final ua = context.watch<UserProvider>(); | ||||||
|     final today = DateTime.now(); |     final dayz = context.watch<SpecialDayProvider>(); | ||||||
|     final birthday = ua.user?.profile?.birthday?.toLocal(); |  | ||||||
|     final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month; |     final days = dayz.getSpecialDays(); | ||||||
|     return Column( |  | ||||||
|       children: [ |     if (days.isNotEmpty) { | ||||||
|         if (isBirthday) |       return Column( | ||||||
|           Card( |           spacing: 8, | ||||||
|             child: ListTile( |           children: days.map((ele) { | ||||||
|               leading: Text('🎂').fontSize(24), |             return Card( | ||||||
|               title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']), |               child: ListTile( | ||||||
|             ), |                 leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24), | ||||||
|           ).padding(bottom: 8), |                 title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']), | ||||||
|       ], |                 subtitle: Text( | ||||||
|     ); |                   DateFormat('y/M/d').format(DateTime.now().copyWith( | ||||||
|  |                     month: kSpecialDays[ele]!.$1, | ||||||
|  |                     day: kSpecialDays[ele]!.$2, | ||||||
|  |                   )), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ).padding(bottom: 8); | ||||||
|  |           }).toList()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final nextOne = dayz.getNextSpecialDay(); | ||||||
|  |     final lastOne = dayz.getLastSpecialDay(); | ||||||
|  |  | ||||||
|  |     if (nextOne != null && lastOne != null) { | ||||||
|  |       var (name, date) = nextOne; | ||||||
|  |       date = date.add(Duration(days: 1)); | ||||||
|  |       final progress = dayz.getSpecialDayProgress(lastOne.$2, date); | ||||||
|  |       final diff = nextOne.$2.add(-const Duration(days: 1)).difference(lastOne.$2); | ||||||
|  |       return Card( | ||||||
|  |         child: ListTile( | ||||||
|  |           leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), | ||||||
|  |           title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]), | ||||||
|  |           subtitle: Row( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |             children: [ | ||||||
|  |               Text('${diff.inDays}d · ${(progress * 100).toStringAsFixed(2)}%'), | ||||||
|  |               const Gap(8), | ||||||
|  |               Expanded( | ||||||
|  |                 child: LinearProgressIndicator( | ||||||
|  |                   value: progress, | ||||||
|  |                   borderRadius: BorderRadius.circular(8), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ).padding(bottom: 8); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return const SizedBox.shrink(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -145,7 +236,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { | |||||||
|       final home = context.read<HomeWidgetProvider>(); |       final home = context.read<HomeWidgetProvider>(); | ||||||
|       final resp = await sn.client.get('/cgi/id/check-in/today'); |       final resp = await sn.client.get('/cgi/id/check-in/today'); | ||||||
|       _todayRecord = SnCheckInRecord.fromJson(resp.data); |       _todayRecord = SnCheckInRecord.fromJson(resp.data); | ||||||
|       home.saveWidgetData('today_check_in', _todayRecord!.toJson()); |       await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson()); | ||||||
|     } finally { |     } finally { | ||||||
|       setState(() => _isBusy = false); |       setState(() => _isBusy = false); | ||||||
|     } |     } | ||||||
| @@ -158,7 +249,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { | |||||||
|       final home = context.read<HomeWidgetProvider>(); |       final home = context.read<HomeWidgetProvider>(); | ||||||
|       final resp = await sn.client.post('/cgi/id/check-in'); |       final resp = await sn.client.post('/cgi/id/check-in'); | ||||||
|       _todayRecord = SnCheckInRecord.fromJson(resp.data); |       _todayRecord = SnCheckInRecord.fromJson(resp.data); | ||||||
|       home.saveWidgetData('today_check_in', _todayRecord!.toJson()); |       await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson()); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       context.showErrorDialog(err); |       context.showErrorDialog(err); | ||||||
| @@ -427,9 +518,7 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati | |||||||
|     setState(() => _isBusy = true); |     setState(() => _isBusy = true); | ||||||
|     try { |     try { | ||||||
|       final pt = context.read<SnPostContentProvider>(); |       final pt = context.read<SnPostContentProvider>(); | ||||||
|       final home = context.read<HomeWidgetProvider>(); |  | ||||||
|       _posts = await pt.listRecommendations(); |       _posts = await pt.listRecommendations(); | ||||||
|       home.saveWidgetData('post_featured', _posts!.map((e) => e.toJson()).toList()); |  | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       context.showErrorDialog(err); |       context.showErrorDialog(err); | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:pasteboard/pasteboard.dart'; | import 'package:pasteboard/pasteboard.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/controllers/post_write_controller.dart'; | import 'package:surface/controllers/post_write_controller.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| @@ -71,11 +72,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final sn = context.read<SnNetworkProvider>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final config = context.read<ConfigProvider>(); | ||||||
|       final resp = await sn.client.get('/cgi/co/publishers/me'); |       final resp = await sn.client.get('/cgi/co/publishers/me'); | ||||||
|       _publishers = List<SnPublisher>.from( |       _publishers = List<SnPublisher>.from( | ||||||
|         resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], |         resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], | ||||||
|       ); |       ); | ||||||
|       _writeController.setPublisher(_publishers?.firstOrNull); |       final beforeId = config.prefs.getInt('int_last_publisher_id'); | ||||||
|  |       _writeController | ||||||
|  |           .setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       context.showErrorDialog(err); |       context.showErrorDialog(err); | ||||||
| @@ -265,6 +269,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | |||||||
|                       }); |                       }); | ||||||
|                     } else { |                     } else { | ||||||
|                       _writeController.setPublisher(value); |                       _writeController.setPublisher(value); | ||||||
|  |                       final config = context.read<ConfigProvider>(); | ||||||
|  |                       config.prefs.setInt('int_last_publisher_id', value.id); | ||||||
|                     } |                     } | ||||||
|                   }, |                   }, | ||||||
|                   buttonStyleData: const ButtonStyleData( |                   buttonStyleData: const ButtonStyleData( | ||||||
| @@ -496,7 +502,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | |||||||
|                           onPressed: (_writeController.isBusy || _writeController.publisher == null) |                           onPressed: (_writeController.isBusy || _writeController.publisher == null) | ||||||
|                               ? null |                               ? null | ||||||
|                               : () { |                               : () { | ||||||
|                                   _writeController.post(context).then((_) { |                                   _writeController.sendPost(context).then((_) { | ||||||
|                                     if (!context.mounted) return; |                                     if (!context.mounted) return; | ||||||
|                                     Navigator.pop(context, true); |                                     Navigator.pop(context, true); | ||||||
|                                   }); |                                   }); | ||||||
|   | |||||||
| @@ -13,7 +13,10 @@ import 'package:surface/widgets/post/post_tags_field.dart'; | |||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||||
|  |  | ||||||
| class PostSearchScreen extends StatefulWidget { | class PostSearchScreen extends StatefulWidget { | ||||||
|   const PostSearchScreen({super.key}); |   final Iterable<String>? initialTags; | ||||||
|  |   final Iterable<String>? initialCategories; | ||||||
|  |  | ||||||
|  |   const PostSearchScreen({super.key, this.initialTags, this.initialCategories}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<PostSearchScreen> createState() => _PostSearchScreenState(); |   State<PostSearchScreen> createState() => _PostSearchScreenState(); | ||||||
| @@ -23,6 +26,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | |||||||
|   bool _isBusy = false; |   bool _isBusy = false; | ||||||
|  |  | ||||||
|   List<String> _searchTags = List.empty(growable: true); |   List<String> _searchTags = List.empty(growable: true); | ||||||
|  |   List<String> _searchCategories = List.empty(growable: true); | ||||||
|  |  | ||||||
|   final List<SnPost> _posts = List.empty(growable: true); |   final List<SnPost> _posts = List.empty(growable: true); | ||||||
|   int? _postCount; |   int? _postCount; | ||||||
| @@ -30,8 +34,18 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | |||||||
|   String _searchTerm = ''; |   String _searchTerm = ''; | ||||||
|   Duration? _lastTook; |   Duration? _lastTook; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _searchTags.addAll(widget.initialTags ?? []); | ||||||
|  |     _searchCategories.addAll(widget.initialCategories ?? []); | ||||||
|  |     if (_searchTags.isNotEmpty || _searchCategories.isNotEmpty) { | ||||||
|  |       _fetchPosts(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<void> _fetchPosts() async { |   Future<void> _fetchPosts() async { | ||||||
|     if (_searchTerm.isEmpty && _searchTags.isEmpty) return; |     if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty) return; | ||||||
|     if (_postCount != null && _posts.length >= _postCount!) return; |     if (_postCount != null && _posts.length >= _postCount!) return; | ||||||
|  |  | ||||||
|     setState(() => _isBusy = true); |     setState(() => _isBusy = true); | ||||||
| @@ -45,6 +59,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | |||||||
|         take: 10, |         take: 10, | ||||||
|         offset: _posts.length, |         offset: _posts.length, | ||||||
|         tags: _searchTags, |         tags: _searchTags, | ||||||
|  |         categories: _searchCategories, | ||||||
|       ); |       ); | ||||||
|       final List<SnPost> out = result.$1; |       final List<SnPost> out = result.$1; | ||||||
|       _postCount = result.$2; |       _postCount = result.$2; | ||||||
| @@ -73,9 +88,25 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | |||||||
|               setState(() => _searchTags = value); |               setState(() => _searchTags = value); | ||||||
|             }, |             }, | ||||||
|           ), |           ), | ||||||
|  |           const Gap(4), | ||||||
|  |           PostCategoriesField( | ||||||
|  |             labelText: 'fieldPostCategories'.tr(), | ||||||
|  |             initialCategories: _searchCategories, | ||||||
|  |             onUpdate: (value) { | ||||||
|  |               setState(() => _searchCategories = value); | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|         ], |         ], | ||||||
|       ).padding(horizontal: 24, vertical: 16), |       ).padding(horizontal: 24, vertical: 16), | ||||||
|     ); |     ).then((_) { | ||||||
|  |       _refreshPosts(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> _refreshPosts() { | ||||||
|  |     _postCount = null; | ||||||
|  |     _posts.clear(); | ||||||
|  |     return _fetchPosts(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -118,8 +149,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | |||||||
|                     setState(() => _posts[idx] = data); |                     setState(() => _posts[idx] = data); | ||||||
|                   }, |                   }, | ||||||
|                   onDeleted: () { |                   onDeleted: () { | ||||||
|                     _posts.clear(); |                     _refreshPosts(); | ||||||
|                     _fetchPosts(); |  | ||||||
|                   }, |                   }, | ||||||
|                 ), |                 ), | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
| @@ -150,10 +180,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | |||||||
|                     _searchTerm = value; |                     _searchTerm = value; | ||||||
|                   }, |                   }, | ||||||
|                   onSubmitted: (value) { |                   onSubmitted: (value) { | ||||||
|                     setState(() => _posts.clear()); |  | ||||||
|  |  | ||||||
|                     _searchTerm = value; |                     _searchTerm = value; | ||||||
|                     _fetchPosts(); |                     _refreshPosts(); | ||||||
|                   }, |                   }, | ||||||
|                 ), |                 ), | ||||||
|                 if (_lastTook != null) |                 if (_lastTook != null) | ||||||
|   | |||||||
| @@ -45,17 +45,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi | |||||||
|   Future<void> _fetchPublisher() async { |   Future<void> _fetchPublisher() async { | ||||||
|     try { |     try { | ||||||
|       final sn = context.read<SnNetworkProvider>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|       final ud = context.read<UserDirectoryProvider>(); |  | ||||||
|       final rel = context.read<SnRelationshipProvider>(); |  | ||||||
|       final resp = await sn.client.get('/cgi/co/publishers/${widget.name}'); |       final resp = await sn.client.get('/cgi/co/publishers/${widget.name}'); | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       _publisher = SnPublisher.fromJson(resp.data); |       _publisher = SnPublisher.fromJson(resp.data); | ||||||
|       _account = await ud.getAccount(_publisher?.accountId); |  | ||||||
|       _accountRelationship = await rel.getRelationship(_account!.id); |  | ||||||
|       if (_publisher?.realmId != null && _publisher!.realmId != 0) { |  | ||||||
|         final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}'); |  | ||||||
|         _realm = SnRealm.fromJson(resp.data); |  | ||||||
|       } |  | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       context.showErrorDialog(err).then((_) { |       context.showErrorDialog(err).then((_) { | ||||||
| @@ -65,6 +57,20 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi | |||||||
|     } finally { |     } finally { | ||||||
|       setState(() {}); |       setState(() {}); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final ud = context.read<UserDirectoryProvider>(); | ||||||
|  |       final rel = context.read<SnRelationshipProvider>(); | ||||||
|  |       _account = await ud.getAccount(_publisher?.accountId); | ||||||
|  |       _accountRelationship = await rel.getRelationship(_account!.id); | ||||||
|  |       if (_publisher?.realmId != null && _publisher!.realmId != 0) { | ||||||
|  |         final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}'); | ||||||
|  |         _realm = SnRealm.fromJson(resp.data); | ||||||
|  |       } | ||||||
|  |     } catch (_) { | ||||||
|  |       // ignore | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool _isSubscribing = false; |   bool _isSubscribing = false; | ||||||
| @@ -277,70 +283,77 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi | |||||||
|               handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), |               handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||||
|               sliver: MultiSliver( |               sliver: MultiSliver( | ||||||
|                 children: [ |                 children: [ | ||||||
|                   SliverAppBar( |                   Theme( | ||||||
|                     expandedHeight: _appBarHeight, |                     data: Theme.of(context).copyWith( | ||||||
|                     title: _publisher == null |                       appBarTheme: Theme.of(context).appBarTheme.copyWith( | ||||||
|                         ? Text('loading').tr() |                         foregroundColor: Colors.white, | ||||||
|                         : RichText( |                       ), | ||||||
|                             textAlign: TextAlign.center, |                     ), | ||||||
|                             text: TextSpan(children: [ |                     child: SliverAppBar( | ||||||
|                               TextSpan( |                       expandedHeight: _appBarHeight, | ||||||
|                                 text: _publisher!.nick, |                       title: _publisher == null | ||||||
|                                 style: Theme.of(context).textTheme.titleLarge!.copyWith( |                           ? Text('loading').tr() | ||||||
|                                       color: Theme.of(context).appBarTheme.foregroundColor!, |                           : RichText( | ||||||
|                                       shadows: labelShadows, |                               textAlign: TextAlign.center, | ||||||
|                                     ), |                               text: TextSpan(children: [ | ||||||
|                               ), |                                 TextSpan( | ||||||
|                               const TextSpan(text: '\n'), |                                   text: _publisher!.nick, | ||||||
|                               TextSpan( |                                   style: Theme.of(context).textTheme.titleLarge!.copyWith( | ||||||
|                                 text: '@${_publisher!.name}', |                                         color: Colors.white, | ||||||
|                                 style: Theme.of(context).textTheme.bodySmall!.copyWith( |                                         shadows: labelShadows, | ||||||
|                                       color: Colors.white, |                                       ), | ||||||
|                                       shadows: labelShadows, |  | ||||||
|                                     ), |  | ||||||
|                               ), |  | ||||||
|                             ]), |  | ||||||
|                           ), |  | ||||||
|                     pinned: true, |  | ||||||
|                     flexibleSpace: _publisher != null |  | ||||||
|                         ? Stack( |  | ||||||
|                             fit: StackFit.expand, |  | ||||||
|                             children: [ |  | ||||||
|                               if (_publisher!.banner.isNotEmpty) |  | ||||||
|                                 UniversalImage( |  | ||||||
|                                   sn.getAttachmentUrl(_publisher!.banner), |  | ||||||
|                                   fit: BoxFit.cover, |  | ||||||
|                                   height: imageHeight, |  | ||||||
|                                   width: _appBarWidth, |  | ||||||
|                                   cacheHeight: imageHeight, |  | ||||||
|                                   cacheWidth: _appBarWidth, |  | ||||||
|                                 ) |  | ||||||
|                               else |  | ||||||
|                                 Container( |  | ||||||
|                                   color: Theme.of(context).colorScheme.surfaceContainer, |  | ||||||
|                                 ), |                                 ), | ||||||
|                               Positioned( |                                 const TextSpan(text: '\n'), | ||||||
|                                 top: 0, |                                 TextSpan( | ||||||
|                                 left: 0, |                                   text: '@${_publisher!.name}', | ||||||
|                                 right: 0, |                                   style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||||
|                                 height: 56 + MediaQuery.of(context).padding.top, |                                         color: Colors.white, | ||||||
|                                 child: ClipRect( |                                         shadows: labelShadows, | ||||||
|                                   child: BackdropFilter( |                                       ), | ||||||
|                                     filter: ImageFilter.blur( |                                 ), | ||||||
|                                       sigmaX: _appBarBlur, |                               ]), | ||||||
|                                       sigmaY: _appBarBlur, |                             ), | ||||||
|                                     ), |                       pinned: true, | ||||||
|                                     child: Container( |                       flexibleSpace: _publisher != null | ||||||
|                                       color: Colors.black.withOpacity( |                           ? Stack( | ||||||
|                                         clampDouble(_appBarBlur * 0.1, 0, 0.5), |                               fit: StackFit.expand, | ||||||
|  |                               children: [ | ||||||
|  |                                 if (_publisher!.banner.isNotEmpty) | ||||||
|  |                                   UniversalImage( | ||||||
|  |                                     sn.getAttachmentUrl(_publisher!.banner), | ||||||
|  |                                     fit: BoxFit.cover, | ||||||
|  |                                     height: imageHeight, | ||||||
|  |                                     width: _appBarWidth, | ||||||
|  |                                     cacheHeight: imageHeight, | ||||||
|  |                                     cacheWidth: _appBarWidth, | ||||||
|  |                                   ) | ||||||
|  |                                 else | ||||||
|  |                                   Container( | ||||||
|  |                                     color: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|  |                                   ), | ||||||
|  |                                 Positioned( | ||||||
|  |                                   top: 0, | ||||||
|  |                                   left: 0, | ||||||
|  |                                   right: 0, | ||||||
|  |                                   height: 56 + MediaQuery.of(context).padding.top, | ||||||
|  |                                   child: ClipRect( | ||||||
|  |                                     child: BackdropFilter( | ||||||
|  |                                       filter: ImageFilter.blur( | ||||||
|  |                                         sigmaX: _appBarBlur, | ||||||
|  |                                         sigmaY: _appBarBlur, | ||||||
|  |                                       ), | ||||||
|  |                                       child: Container( | ||||||
|  |                                         color: Colors.black.withOpacity( | ||||||
|  |                                           clampDouble(_appBarBlur * 0.1, 0, 0.5), | ||||||
|  |                                         ), | ||||||
|                                       ), |                                       ), | ||||||
|                                     ), |                                     ), | ||||||
|                                   ), |                                   ), | ||||||
|                                 ), |                                 ), | ||||||
|                               ), |                               ], | ||||||
|                             ], |                             ) | ||||||
|                           ) |                           : null, | ||||||
|                         : null, |                     ), | ||||||
|                   ), |                   ), | ||||||
|                   if (_publisher != null) |                   if (_publisher != null) | ||||||
|                     SliverToBoxAdapter( |                     SliverToBoxAdapter( | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
|  | import 'package:collection/collection.dart'; | ||||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | import 'package:dropdown_button2/dropdown_button2.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:flutter_colorpicker/flutter_colorpicker.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:image_picker/image_picker.dart'; | import 'package:image_picker/image_picker.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| @@ -12,11 +13,23 @@ import 'package:path_provider/path_provider.dart'; | |||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
| import 'package:styled_widget/styled_widget.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/sn_network.dart'; | ||||||
| import 'package:surface/providers/theme.dart'; | import 'package:surface/providers/theme.dart'; | ||||||
| import 'package:surface/theme.dart'; | import 'package:surface/theme.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  |  | ||||||
|  | const Map<String, Color> kColorSchemes = { | ||||||
|  |   'colorSchemeIndigo': Colors.indigo, | ||||||
|  |   'colorSchemeBlue': Colors.blue, | ||||||
|  |   'colorSchemeGreen': Colors.green, | ||||||
|  |   'colorSchemeYellow': Colors.yellow, | ||||||
|  |   'colorSchemeOrange': Colors.orange, | ||||||
|  |   'colorSchemeRed': Colors.red, | ||||||
|  |   'colorSchemeWhite': Colors.white, | ||||||
|  |   'colorSchemeBlack': Colors.black, | ||||||
|  | }; | ||||||
|  |  | ||||||
| class SettingsScreen extends StatefulWidget { | class SettingsScreen extends StatefulWidget { | ||||||
|   const SettingsScreen({super.key}); |   const SettingsScreen({super.key}); | ||||||
|  |  | ||||||
| @@ -25,7 +38,7 @@ class SettingsScreen extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _SettingsScreenState extends State<SettingsScreen> { | class _SettingsScreenState extends State<SettingsScreen> { | ||||||
|   SharedPreferences? _prefs; |   late final SharedPreferences _prefs; | ||||||
|   String _docBasepath = '/'; |   String _docBasepath = '/'; | ||||||
|  |  | ||||||
|   final TextEditingController _serverUrlController = TextEditingController(); |   final TextEditingController _serverUrlController = TextEditingController(); | ||||||
| @@ -39,12 +52,9 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|         setState(() {}); |         setState(() {}); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|     SharedPreferences.getInstance().then((prefs) { |     final config = context.read<ConfigProvider>(); | ||||||
|       setState(() { |     _prefs = config.prefs; | ||||||
|         _prefs = prefs; |     _serverUrlController.text = config.serverUrl; | ||||||
|         _serverUrlController.text = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -60,6 +70,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       body: SingleChildScrollView( |       body: SingleChildScrollView( | ||||||
|         child: Column( |         child: Column( | ||||||
|  |           spacing: 16, | ||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           children: [ |           children: [ | ||||||
|             Column( |             Column( | ||||||
| @@ -78,7 +89,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                       if (image == null) return; |                       if (image == null) return; | ||||||
|  |  | ||||||
|                       await File(image.path).copy('$_docBasepath/app_background_image'); |                       await File(image.path).copy('$_docBasepath/app_background_image'); | ||||||
|                       _prefs?.setBool('has_background_image', true); |                       _prefs.setBool(kAppBackgroundStoreKey, true); | ||||||
|  |  | ||||||
|                       setState(() {}); |                       setState(() {}); | ||||||
|                     }, |                     }, | ||||||
| @@ -99,29 +110,136 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                           trailing: const Icon(Symbols.chevron_right), |                           trailing: const Icon(Symbols.chevron_right), | ||||||
|                           onTap: () { |                           onTap: () { | ||||||
|                             File('$_docBasepath/app_background_image').deleteSync(); |                             File('$_docBasepath/app_background_image').deleteSync(); | ||||||
|                             _prefs?.remove('has_background_image'); |                             _prefs.remove(kAppBackgroundStoreKey); | ||||||
|                             setState(() {}); |                             setState(() {}); | ||||||
|                           }, |                           }, | ||||||
|                         ); |                         ); | ||||||
|                       }), |                       }), | ||||||
|                 if (_prefs != null) |                 CheckboxListTile( | ||||||
|                   CheckboxListTile( |                   title: Text('settingsThemeMaterial3').tr(), | ||||||
|                     title: Text('settingsThemeMaterial3').tr(), |                   subtitle: Text('settingsThemeMaterial3Description').tr(), | ||||||
|                     subtitle: Text('settingsThemeMaterial3Description').tr(), |                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||||
|                     contentPadding: const EdgeInsets.only(left: 24, right: 17), |                   secondary: const Icon(Symbols.new_releases), | ||||||
|                     secondary: const Icon(Symbols.new_releases), |                   value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false, | ||||||
|                     value: _prefs!.getBool(kMaterialYouToggleStoreKey) ?? false, |                   onChanged: (value) { | ||||||
|                     onChanged: (value) { |                     setState(() { | ||||||
|                       setState(() { |                       _prefs.setBool( | ||||||
|                         _prefs!.setBool( |                         kMaterialYouToggleStoreKey, | ||||||
|                           kMaterialYouToggleStoreKey, |                         value ?? false, | ||||||
|                           value ?? false, |                       ); | ||||||
|                         ); |                     }); | ||||||
|                       }); |                     final th = context.read<ThemeProvider>(); | ||||||
|                       final th = context.watch<ThemeProvider>(); |                     th.reloadTheme(useMaterial3: value ?? false); | ||||||
|                       th.reloadTheme(useMaterial3: value ?? false); |                   }, | ||||||
|                     }, |                 ), | ||||||
|  |                 ListTile( | ||||||
|  |                   leading: const Icon(Symbols.format_paint), | ||||||
|  |                   title: Text('settingsColorScheme').tr(), | ||||||
|  |                   subtitle: Text('settingsColorSchemeDescription').tr(), | ||||||
|  |                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |                   trailing: const Icon(Symbols.chevron_right), | ||||||
|  |                   onTap: () async { | ||||||
|  |                     Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value); | ||||||
|  |                     final color = await showDialog<Color?>( | ||||||
|  |                       context: context, | ||||||
|  |                       builder: (context) => AlertDialog( | ||||||
|  |                         content: SingleChildScrollView( | ||||||
|  |                           child: ColorPicker( | ||||||
|  |                             pickerColor: pickerColor, | ||||||
|  |                             onColorChanged: (color) => pickerColor = color, | ||||||
|  |                             enableAlpha: false, | ||||||
|  |                             hexInputBar: true, | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         actions: <Widget>[ | ||||||
|  |                           TextButton( | ||||||
|  |                             child: const Text('dialogDismiss').tr(), | ||||||
|  |                             onPressed: () { | ||||||
|  |                               Navigator.of(context).pop(); | ||||||
|  |                             }, | ||||||
|  |                           ), | ||||||
|  |                           TextButton( | ||||||
|  |                             child: const Text('dialogConfirm').tr(), | ||||||
|  |                             onPressed: () { | ||||||
|  |                               Navigator.of(context).pop(pickerColor); | ||||||
|  |                             }, | ||||||
|  |                           ), | ||||||
|  |                         ], | ||||||
|  |                       ), | ||||||
|  |                     ); | ||||||
|  |  | ||||||
|  |                     if (color == null || !context.mounted) return; | ||||||
|  |  | ||||||
|  |                     _prefs.setInt(kAppColorSchemeStoreKey, color.value); | ||||||
|  |                     final th = context.read<ThemeProvider>(); | ||||||
|  |                     th.reloadTheme(seedColorOverride: color); | ||||||
|  |                     setState(() {}); | ||||||
|  |  | ||||||
|  |                     context.showSnackbar('colorSchemeApplied'.tr()); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|  |                 ListTile( | ||||||
|  |                   leading: const Icon(Symbols.palette), | ||||||
|  |                   title: Text('settingsColorSeed').tr(), | ||||||
|  |                   subtitle: Text('settingsColorSeedDescription').tr(), | ||||||
|  |                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||||
|  |                   trailing: DropdownButtonHideUnderline( | ||||||
|  |                     child: DropdownButton2<int?>( | ||||||
|  |                       isExpanded: true, | ||||||
|  |                       items: [ | ||||||
|  |                         ...kColorSchemes.entries.mapIndexed((idx, ele) { | ||||||
|  |                           return DropdownMenuItem<int>( | ||||||
|  |                             value: idx, | ||||||
|  |                             child: Text(ele.key).tr(), | ||||||
|  |                           ); | ||||||
|  |                         }), | ||||||
|  |                         DropdownMenuItem<int>( | ||||||
|  |                           value: -1, | ||||||
|  |                           child: Text('custom').tr(), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                       value: _prefs.getInt(kAppColorSchemeStoreKey) == null | ||||||
|  |                           ? 1 | ||||||
|  |                           : kColorSchemes.values | ||||||
|  |                               .toList() | ||||||
|  |                               .indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)), | ||||||
|  |                       onChanged: (int? value) { | ||||||
|  |                         if (value != null && value != -1) { | ||||||
|  |                           _prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value); | ||||||
|  |                           final th = context.read<ThemeProvider>(); | ||||||
|  |                           th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value)); | ||||||
|  |                           setState(() {}); | ||||||
|  |  | ||||||
|  |                           context.showSnackbar('colorSchemeApplied'.tr()); | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                       buttonStyleData: const ButtonStyleData( | ||||||
|  |                         padding: EdgeInsets.symmetric( | ||||||
|  |                           horizontal: 16, | ||||||
|  |                           vertical: 5, | ||||||
|  |                         ), | ||||||
|  |                         height: 40, | ||||||
|  |                         width: 160, | ||||||
|  |                       ), | ||||||
|  |                       menuItemStyleData: const MenuItemStyleData( | ||||||
|  |                         height: 40, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|                   ), |                   ), | ||||||
|  |                 ), | ||||||
|  |                 CheckboxListTile( | ||||||
|  |                   secondary: const Icon(Symbols.blur_on), | ||||||
|  |                   title: Text('settingsAppBarTransparent').tr(), | ||||||
|  |                   subtitle: Text('settingsAppBarTransparentDescription').tr(), | ||||||
|  |                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||||
|  |                   value: _prefs.getBool(kAppbarTransparentStoreKey) ?? false, | ||||||
|  |                   onChanged: (value) { | ||||||
|  |                     _prefs.setBool(kAppbarTransparentStoreKey, value ?? false); | ||||||
|  |                     final th = context.read<ThemeProvider>(); | ||||||
|  |                     th.reloadTheme(); | ||||||
|  |                     setState(() {}); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|             Column( |             Column( | ||||||
| @@ -139,7 +257,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                       icon: const Icon(Symbols.save), |                       icon: const Icon(Symbols.save), | ||||||
|                       onPressed: () { |                       onPressed: () { | ||||||
|                         sn.setBaseUrl(_serverUrlController.text); |                         sn.setBaseUrl(_serverUrlController.text); | ||||||
|                         _prefs?.setString( |                         _prefs.setString( | ||||||
|                           kNetworkServerStoreKey, |                           kNetworkServerStoreKey, | ||||||
|                           _serverUrlController.text, |                           _serverUrlController.text, | ||||||
|                         ); |                         ); | ||||||
| @@ -182,7 +300,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                       onChanged: (String? value) { |                       onChanged: (String? value) { | ||||||
|                         if (value == null) return; |                         if (value == null) return; | ||||||
|                         _serverUrlController.text = value; |                         _serverUrlController.text = value; | ||||||
|                         _prefs?.setString(kNetworkServerStoreKey, value); |                         _prefs.setString(kNetworkServerStoreKey, value); | ||||||
|                         context.showSnackbar('settingsNetworkServerSaved'.tr()); |                         context.showSnackbar('settingsNetworkServerSaved'.tr()); | ||||||
|                         setState(() {}); |                         setState(() {}); | ||||||
|                       }, |                       }, | ||||||
| @@ -191,7 +309,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                           horizontal: 16, |                           horizontal: 16, | ||||||
|                           vertical: 5, |                           vertical: 5, | ||||||
|                         ), |                         ), | ||||||
|                         height: 40, |                         height: 56, | ||||||
|                         width: 160, |                         width: 160, | ||||||
|                       ), |                       ), | ||||||
|                       menuItemStyleData: const MenuItemStyleData( |                       menuItemStyleData: const MenuItemStyleData( | ||||||
| @@ -208,13 +326,56 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                   trailing: const Icon(Symbols.chevron_right), |                   trailing: const Icon(Symbols.chevron_right), | ||||||
|                   onTap: () { |                   onTap: () { | ||||||
|                     _serverUrlController.text = kNetworkServerDefault; |                     _serverUrlController.text = kNetworkServerDefault; | ||||||
|                     _prefs?.remove(kNetworkServerStoreKey); |                     _prefs.remove(kNetworkServerStoreKey); | ||||||
|                     context.showSnackbar('settingsNetworkServerSaved'.tr()); |                     context.showSnackbar('settingsNetworkServerSaved'.tr()); | ||||||
|                     setState(() {}); |                     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( |             Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
| @@ -231,7 +392,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|           ].expand((ele) => [ele, const Gap(16)]).toList(), |           ], | ||||||
|         ).padding(vertical: 20), |         ).padding(vertical: 20), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -1,16 +1,16 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:developer'; | import 'dart:developer'; | ||||||
|  | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:cross_file/cross_file.dart'; | import 'package:cross_file/cross_file.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/scheduler.dart'; |  | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:receive_sharing_intent/receive_sharing_intent.dart'; | import 'package:receive_sharing_intent/receive_sharing_intent.dart'; | ||||||
| import 'package:surface/controllers/post_write_controller.dart'; | import 'package:surface/controllers/post_write_controller.dart'; | ||||||
| import 'package:surface/screens/post/post_editor.dart'; | import 'package:surface/screens/post/post_editor.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; |  | ||||||
|  |  | ||||||
| class AppSharingListener extends StatefulWidget { | class AppSharingListener extends StatefulWidget { | ||||||
|   final Widget child; |   final Widget child; | ||||||
| @@ -103,8 +103,10 @@ class _AppSharingListenerState extends State<AppSharingListener> { | |||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _initialize(); |     if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { | ||||||
|     _initialHandle(); |       _initialize(); | ||||||
|  |       _initialHandle(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
|  |  | ||||||
| const kMaterialYouToggleStoreKey = 'app_theme_material_you'; | const kMaterialYouToggleStoreKey = 'app_theme_material_you'; | ||||||
|  |  | ||||||
| @@ -10,7 +11,7 @@ class ThemeSet { | |||||||
|   ThemeSet({required this.light, required this.dark}); |   ThemeSet({required this.light, required this.dark}); | ||||||
| } | } | ||||||
|  |  | ||||||
| Future<ThemeSet> createAppThemeSet({bool? useMaterial3}) async { | Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async { | ||||||
|   return ThemeSet( |   return ThemeSet( | ||||||
|     light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3), |     light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3), | ||||||
|     dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3), |     dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3), | ||||||
| @@ -19,16 +20,21 @@ Future<ThemeSet> createAppThemeSet({bool? useMaterial3}) async { | |||||||
|  |  | ||||||
| Future<ThemeData> createAppTheme( | Future<ThemeData> createAppTheme( | ||||||
|   Brightness brightness, { |   Brightness brightness, { | ||||||
|  |     Color? seedColorOverride, | ||||||
|   bool? useMaterial3, |   bool? useMaterial3, | ||||||
| }) async { | }) async { | ||||||
|   final prefs = await SharedPreferences.getInstance(); |   final prefs = await SharedPreferences.getInstance(); | ||||||
|  |  | ||||||
|  |   final seedColorString = prefs.getInt(kAppColorSchemeStoreKey); | ||||||
|  |   final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo; | ||||||
|  |  | ||||||
|   final colorScheme = ColorScheme.fromSeed( |   final colorScheme = ColorScheme.fromSeed( | ||||||
|     seedColor: Colors.indigo, |     seedColor: seedColorOverride ?? seedColor, | ||||||
|     brightness: brightness, |     brightness: brightness, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   final hasBackground = prefs.getBool('has_background_image') ?? false; |   final hasBackground = prefs.getBool(kAppBackgroundStoreKey) ?? false; | ||||||
|  |   final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||||
|  |  | ||||||
|   return ThemeData( |   return ThemeData( | ||||||
|     useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), |     useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), | ||||||
| @@ -42,8 +48,9 @@ Future<ThemeData> createAppTheme( | |||||||
|     ), |     ), | ||||||
|     appBarTheme: AppBarTheme( |     appBarTheme: AppBarTheme( | ||||||
|       centerTitle: true, |       centerTitle: true, | ||||||
|       backgroundColor: hasBackground ? colorScheme.primary.withOpacity(0.75) : colorScheme.primary, |       elevation: hasAppBarBlurry ? 0 : null, | ||||||
|       foregroundColor: colorScheme.onPrimary, |       backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary, | ||||||
|  |       foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, | ||||||
|     ), |     ), | ||||||
|     scaffoldBackgroundColor: Colors.transparent, |     scaffoldBackgroundColor: Colors.transparent, | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; | |||||||
| part 'check_in.freezed.dart'; | part 'check_in.freezed.dart'; | ||||||
| part 'check_in.g.dart'; | part 'check_in.g.dart'; | ||||||
|  |  | ||||||
|  | const List<String> kCheckInResultTierSymbols = ['大凶', '凶', '中平', '吉', '大吉']; | ||||||
|  |  | ||||||
| @freezed | @freezed | ||||||
| class SnCheckInRecord with _$SnCheckInRecord { | class SnCheckInRecord with _$SnCheckInRecord { | ||||||
|   const SnCheckInRecord._(); |   const SnCheckInRecord._(); | ||||||
| @@ -21,11 +23,5 @@ class SnCheckInRecord with _$SnCheckInRecord { | |||||||
|   factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => |   factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => | ||||||
|       _$SnCheckInRecordFromJson(json); |       _$SnCheckInRecordFromJson(json); | ||||||
|  |  | ||||||
|   String get symbol => switch (resultTier) { |   String get symbol => kCheckInResultTierSymbols[resultTier]; | ||||||
|         0 => '大凶', |  | ||||||
|         1 => '凶', |  | ||||||
|         2 => '中平', |  | ||||||
|         3 => '吉', |  | ||||||
|         _ => '大吉', |  | ||||||
|       }; |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ class SnPost with _$SnPost { | |||||||
|     required String? alias, |     required String? alias, | ||||||
|     required String? aliasPrefix, |     required String? aliasPrefix, | ||||||
|     @Default([]) List<SnPostTag> tags, |     @Default([]) List<SnPostTag> tags, | ||||||
|     @Default([]) List<dynamic> categories, |     @Default([]) List<SnPostCategory> categories, | ||||||
|     required List<SnPost>? replies, |     required List<SnPost>? replies, | ||||||
|     required int? replyId, |     required int? replyId, | ||||||
|     required int? repostId, |     required int? repostId, | ||||||
| @@ -67,6 +67,23 @@ class SnPostTag with _$SnPostTag { | |||||||
|       _$SnPostTagFromJson(json); |       _$SnPostTagFromJson(json); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @freezed | ||||||
|  | class SnPostCategory with _$SnPostCategory { | ||||||
|  |   const factory SnPostCategory({ | ||||||
|  |     required int id, | ||||||
|  |     required DateTime createdAt, | ||||||
|  |     required DateTime updatedAt, | ||||||
|  |     required dynamic deletedAt, | ||||||
|  |     required String alias, | ||||||
|  |     required String name, | ||||||
|  |     required String description, | ||||||
|  |     required dynamic posts, | ||||||
|  |   }) = _SnPostCategory; | ||||||
|  |  | ||||||
|  |   factory SnPostCategory.fromJson(Map<String, Object?> json) => | ||||||
|  |       _$SnPostCategoryFromJson(json); | ||||||
|  | } | ||||||
|  |  | ||||||
| @freezed | @freezed | ||||||
| class SnPostPreload with _$SnPostPreload { | class SnPostPreload with _$SnPostPreload { | ||||||
|   const factory SnPostPreload({ |   const factory SnPostPreload({ | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ mixin _$SnPost { | |||||||
|   String? get alias => throw _privateConstructorUsedError; |   String? get alias => throw _privateConstructorUsedError; | ||||||
|   String? get aliasPrefix => throw _privateConstructorUsedError; |   String? get aliasPrefix => throw _privateConstructorUsedError; | ||||||
|   List<SnPostTag> get tags => throw _privateConstructorUsedError; |   List<SnPostTag> get tags => throw _privateConstructorUsedError; | ||||||
|   List<dynamic> get categories => throw _privateConstructorUsedError; |   List<SnPostCategory> get categories => throw _privateConstructorUsedError; | ||||||
|   List<SnPost>? get replies => throw _privateConstructorUsedError; |   List<SnPost>? get replies => throw _privateConstructorUsedError; | ||||||
|   int? get replyId => throw _privateConstructorUsedError; |   int? get replyId => throw _privateConstructorUsedError; | ||||||
|   int? get repostId => throw _privateConstructorUsedError; |   int? get repostId => throw _privateConstructorUsedError; | ||||||
| @@ -77,7 +77,7 @@ abstract class $SnPostCopyWith<$Res> { | |||||||
|       String? alias, |       String? alias, | ||||||
|       String? aliasPrefix, |       String? aliasPrefix, | ||||||
|       List<SnPostTag> tags, |       List<SnPostTag> tags, | ||||||
|       List<dynamic> categories, |       List<SnPostCategory> categories, | ||||||
|       List<SnPost>? replies, |       List<SnPost>? replies, | ||||||
|       int? replyId, |       int? replyId, | ||||||
|       int? repostId, |       int? repostId, | ||||||
| @@ -197,7 +197,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | |||||||
|       categories: null == categories |       categories: null == categories | ||||||
|           ? _value.categories |           ? _value.categories | ||||||
|           : categories // ignore: cast_nullable_to_non_nullable |           : categories // ignore: cast_nullable_to_non_nullable | ||||||
|               as List<dynamic>, |               as List<SnPostCategory>, | ||||||
|       replies: freezed == replies |       replies: freezed == replies | ||||||
|           ? _value.replies |           ? _value.replies | ||||||
|           : replies // ignore: cast_nullable_to_non_nullable |           : replies // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -362,7 +362,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { | |||||||
|       String? alias, |       String? alias, | ||||||
|       String? aliasPrefix, |       String? aliasPrefix, | ||||||
|       List<SnPostTag> tags, |       List<SnPostTag> tags, | ||||||
|       List<dynamic> categories, |       List<SnPostCategory> categories, | ||||||
|       List<SnPost>? replies, |       List<SnPost>? replies, | ||||||
|       int? replyId, |       int? replyId, | ||||||
|       int? repostId, |       int? repostId, | ||||||
| @@ -485,7 +485,7 @@ class __$$SnPostImplCopyWithImpl<$Res> | |||||||
|       categories: null == categories |       categories: null == categories | ||||||
|           ? _value._categories |           ? _value._categories | ||||||
|           : categories // ignore: cast_nullable_to_non_nullable |           : categories // ignore: cast_nullable_to_non_nullable | ||||||
|               as List<dynamic>, |               as List<SnPostCategory>, | ||||||
|       replies: freezed == replies |       replies: freezed == replies | ||||||
|           ? _value._replies |           ? _value._replies | ||||||
|           : replies // ignore: cast_nullable_to_non_nullable |           : replies // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -584,7 +584,7 @@ class _$SnPostImpl extends _SnPost { | |||||||
|       required this.alias, |       required this.alias, | ||||||
|       required this.aliasPrefix, |       required this.aliasPrefix, | ||||||
|       final List<SnPostTag> tags = const [], |       final List<SnPostTag> tags = const [], | ||||||
|       final List<dynamic> categories = const [], |       final List<SnPostCategory> categories = const [], | ||||||
|       required final List<SnPost>? replies, |       required final List<SnPost>? replies, | ||||||
|       required this.replyId, |       required this.replyId, | ||||||
|       required this.repostId, |       required this.repostId, | ||||||
| @@ -649,10 +649,10 @@ class _$SnPostImpl extends _SnPost { | |||||||
|     return EqualUnmodifiableListView(_tags); |     return EqualUnmodifiableListView(_tags); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final List<dynamic> _categories; |   final List<SnPostCategory> _categories; | ||||||
|   @override |   @override | ||||||
|   @JsonKey() |   @JsonKey() | ||||||
|   List<dynamic> get categories { |   List<SnPostCategory> get categories { | ||||||
|     if (_categories is EqualUnmodifiableListView) return _categories; |     if (_categories is EqualUnmodifiableListView) return _categories; | ||||||
|     // ignore: implicit_dynamic_type |     // ignore: implicit_dynamic_type | ||||||
|     return EqualUnmodifiableListView(_categories); |     return EqualUnmodifiableListView(_categories); | ||||||
| @@ -853,7 +853,7 @@ abstract class _SnPost extends SnPost { | |||||||
|       required final String? alias, |       required final String? alias, | ||||||
|       required final String? aliasPrefix, |       required final String? aliasPrefix, | ||||||
|       final List<SnPostTag> tags, |       final List<SnPostTag> tags, | ||||||
|       final List<dynamic> categories, |       final List<SnPostCategory> categories, | ||||||
|       required final List<SnPost>? replies, |       required final List<SnPost>? replies, | ||||||
|       required final int? replyId, |       required final int? replyId, | ||||||
|       required final int? repostId, |       required final int? repostId, | ||||||
| @@ -899,7 +899,7 @@ abstract class _SnPost extends SnPost { | |||||||
|   @override |   @override | ||||||
|   List<SnPostTag> get tags; |   List<SnPostTag> get tags; | ||||||
|   @override |   @override | ||||||
|   List<dynamic> get categories; |   List<SnPostCategory> get categories; | ||||||
|   @override |   @override | ||||||
|   List<SnPost>? get replies; |   List<SnPost>? get replies; | ||||||
|   @override |   @override | ||||||
| @@ -1253,6 +1253,312 @@ abstract class _SnPostTag implements SnPostTag { | |||||||
|       throw _privateConstructorUsedError; |       throw _privateConstructorUsedError; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) { | ||||||
|  |   return _SnPostCategory.fromJson(json); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | mixin _$SnPostCategory { | ||||||
|  |   int get id => throw _privateConstructorUsedError; | ||||||
|  |   DateTime get createdAt => throw _privateConstructorUsedError; | ||||||
|  |   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||||
|  |   dynamic get deletedAt => throw _privateConstructorUsedError; | ||||||
|  |   String get alias => throw _privateConstructorUsedError; | ||||||
|  |   String get name => throw _privateConstructorUsedError; | ||||||
|  |   String get description => throw _privateConstructorUsedError; | ||||||
|  |   dynamic get posts => throw _privateConstructorUsedError; | ||||||
|  |  | ||||||
|  |   /// Serializes this SnPostCategory to a JSON map. | ||||||
|  |   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||||
|  |  | ||||||
|  |   /// Create a copy of SnPostCategory | ||||||
|  |   /// with the given fields replaced by the non-null parameter values. | ||||||
|  |   @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  |   $SnPostCategoryCopyWith<SnPostCategory> get copyWith => | ||||||
|  |       throw _privateConstructorUsedError; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract class $SnPostCategoryCopyWith<$Res> { | ||||||
|  |   factory $SnPostCategoryCopyWith( | ||||||
|  |           SnPostCategory value, $Res Function(SnPostCategory) then) = | ||||||
|  |       _$SnPostCategoryCopyWithImpl<$Res, SnPostCategory>; | ||||||
|  |   @useResult | ||||||
|  |   $Res call( | ||||||
|  |       {int id, | ||||||
|  |       DateTime createdAt, | ||||||
|  |       DateTime updatedAt, | ||||||
|  |       dynamic deletedAt, | ||||||
|  |       String alias, | ||||||
|  |       String name, | ||||||
|  |       String description, | ||||||
|  |       dynamic posts}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | class _$SnPostCategoryCopyWithImpl<$Res, $Val extends SnPostCategory> | ||||||
|  |     implements $SnPostCategoryCopyWith<$Res> { | ||||||
|  |   _$SnPostCategoryCopyWithImpl(this._value, this._then); | ||||||
|  |  | ||||||
|  |   // ignore: unused_field | ||||||
|  |   final $Val _value; | ||||||
|  |   // ignore: unused_field | ||||||
|  |   final $Res Function($Val) _then; | ||||||
|  |  | ||||||
|  |   /// Create a copy of SnPostCategory | ||||||
|  |   /// with the given fields replaced by the non-null parameter values. | ||||||
|  |   @pragma('vm:prefer-inline') | ||||||
|  |   @override | ||||||
|  |   $Res call({ | ||||||
|  |     Object? id = null, | ||||||
|  |     Object? createdAt = null, | ||||||
|  |     Object? updatedAt = null, | ||||||
|  |     Object? deletedAt = freezed, | ||||||
|  |     Object? alias = null, | ||||||
|  |     Object? name = null, | ||||||
|  |     Object? description = null, | ||||||
|  |     Object? posts = freezed, | ||||||
|  |   }) { | ||||||
|  |     return _then(_value.copyWith( | ||||||
|  |       id: null == id | ||||||
|  |           ? _value.id | ||||||
|  |           : id // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as int, | ||||||
|  |       createdAt: null == createdAt | ||||||
|  |           ? _value.createdAt | ||||||
|  |           : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as DateTime, | ||||||
|  |       updatedAt: null == updatedAt | ||||||
|  |           ? _value.updatedAt | ||||||
|  |           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as DateTime, | ||||||
|  |       deletedAt: freezed == deletedAt | ||||||
|  |           ? _value.deletedAt | ||||||
|  |           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as dynamic, | ||||||
|  |       alias: null == alias | ||||||
|  |           ? _value.alias | ||||||
|  |           : alias // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|  |       name: null == name | ||||||
|  |           ? _value.name | ||||||
|  |           : name // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|  |       description: null == description | ||||||
|  |           ? _value.description | ||||||
|  |           : description // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|  |       posts: freezed == posts | ||||||
|  |           ? _value.posts | ||||||
|  |           : posts // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as dynamic, | ||||||
|  |     ) as $Val); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract class _$$SnPostCategoryImplCopyWith<$Res> | ||||||
|  |     implements $SnPostCategoryCopyWith<$Res> { | ||||||
|  |   factory _$$SnPostCategoryImplCopyWith(_$SnPostCategoryImpl value, | ||||||
|  |           $Res Function(_$SnPostCategoryImpl) then) = | ||||||
|  |       __$$SnPostCategoryImplCopyWithImpl<$Res>; | ||||||
|  |   @override | ||||||
|  |   @useResult | ||||||
|  |   $Res call( | ||||||
|  |       {int id, | ||||||
|  |       DateTime createdAt, | ||||||
|  |       DateTime updatedAt, | ||||||
|  |       dynamic deletedAt, | ||||||
|  |       String alias, | ||||||
|  |       String name, | ||||||
|  |       String description, | ||||||
|  |       dynamic posts}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | class __$$SnPostCategoryImplCopyWithImpl<$Res> | ||||||
|  |     extends _$SnPostCategoryCopyWithImpl<$Res, _$SnPostCategoryImpl> | ||||||
|  |     implements _$$SnPostCategoryImplCopyWith<$Res> { | ||||||
|  |   __$$SnPostCategoryImplCopyWithImpl( | ||||||
|  |       _$SnPostCategoryImpl _value, $Res Function(_$SnPostCategoryImpl) _then) | ||||||
|  |       : super(_value, _then); | ||||||
|  |  | ||||||
|  |   /// Create a copy of SnPostCategory | ||||||
|  |   /// with the given fields replaced by the non-null parameter values. | ||||||
|  |   @pragma('vm:prefer-inline') | ||||||
|  |   @override | ||||||
|  |   $Res call({ | ||||||
|  |     Object? id = null, | ||||||
|  |     Object? createdAt = null, | ||||||
|  |     Object? updatedAt = null, | ||||||
|  |     Object? deletedAt = freezed, | ||||||
|  |     Object? alias = null, | ||||||
|  |     Object? name = null, | ||||||
|  |     Object? description = null, | ||||||
|  |     Object? posts = freezed, | ||||||
|  |   }) { | ||||||
|  |     return _then(_$SnPostCategoryImpl( | ||||||
|  |       id: null == id | ||||||
|  |           ? _value.id | ||||||
|  |           : id // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as int, | ||||||
|  |       createdAt: null == createdAt | ||||||
|  |           ? _value.createdAt | ||||||
|  |           : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as DateTime, | ||||||
|  |       updatedAt: null == updatedAt | ||||||
|  |           ? _value.updatedAt | ||||||
|  |           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as DateTime, | ||||||
|  |       deletedAt: freezed == deletedAt | ||||||
|  |           ? _value.deletedAt | ||||||
|  |           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as dynamic, | ||||||
|  |       alias: null == alias | ||||||
|  |           ? _value.alias | ||||||
|  |           : alias // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|  |       name: null == name | ||||||
|  |           ? _value.name | ||||||
|  |           : name // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|  |       description: null == description | ||||||
|  |           ? _value.description | ||||||
|  |           : description // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|  |       posts: freezed == posts | ||||||
|  |           ? _value.posts | ||||||
|  |           : posts // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as dynamic, | ||||||
|  |     )); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | @JsonSerializable() | ||||||
|  | class _$SnPostCategoryImpl implements _SnPostCategory { | ||||||
|  |   const _$SnPostCategoryImpl( | ||||||
|  |       {required this.id, | ||||||
|  |       required this.createdAt, | ||||||
|  |       required this.updatedAt, | ||||||
|  |       required this.deletedAt, | ||||||
|  |       required this.alias, | ||||||
|  |       required this.name, | ||||||
|  |       required this.description, | ||||||
|  |       required this.posts}); | ||||||
|  |  | ||||||
|  |   factory _$SnPostCategoryImpl.fromJson(Map<String, dynamic> json) => | ||||||
|  |       _$$SnPostCategoryImplFromJson(json); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   final int id; | ||||||
|  |   @override | ||||||
|  |   final DateTime createdAt; | ||||||
|  |   @override | ||||||
|  |   final DateTime updatedAt; | ||||||
|  |   @override | ||||||
|  |   final dynamic deletedAt; | ||||||
|  |   @override | ||||||
|  |   final String alias; | ||||||
|  |   @override | ||||||
|  |   final String name; | ||||||
|  |   @override | ||||||
|  |   final String description; | ||||||
|  |   @override | ||||||
|  |   final dynamic posts; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toString() { | ||||||
|  |     return 'SnPostCategory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, posts: $posts)'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) { | ||||||
|  |     return identical(this, other) || | ||||||
|  |         (other.runtimeType == runtimeType && | ||||||
|  |             other is _$SnPostCategoryImpl && | ||||||
|  |             (identical(other.id, id) || other.id == id) && | ||||||
|  |             (identical(other.createdAt, createdAt) || | ||||||
|  |                 other.createdAt == createdAt) && | ||||||
|  |             (identical(other.updatedAt, updatedAt) || | ||||||
|  |                 other.updatedAt == updatedAt) && | ||||||
|  |             const DeepCollectionEquality().equals(other.deletedAt, deletedAt) && | ||||||
|  |             (identical(other.alias, alias) || other.alias == alias) && | ||||||
|  |             (identical(other.name, name) || other.name == name) && | ||||||
|  |             (identical(other.description, description) || | ||||||
|  |                 other.description == description) && | ||||||
|  |             const DeepCollectionEquality().equals(other.posts, posts)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  |   @override | ||||||
|  |   int get hashCode => Object.hash( | ||||||
|  |       runtimeType, | ||||||
|  |       id, | ||||||
|  |       createdAt, | ||||||
|  |       updatedAt, | ||||||
|  |       const DeepCollectionEquality().hash(deletedAt), | ||||||
|  |       alias, | ||||||
|  |       name, | ||||||
|  |       description, | ||||||
|  |       const DeepCollectionEquality().hash(posts)); | ||||||
|  |  | ||||||
|  |   /// Create a copy of SnPostCategory | ||||||
|  |   /// with the given fields replaced by the non-null parameter values. | ||||||
|  |   @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  |   @override | ||||||
|  |   @pragma('vm:prefer-inline') | ||||||
|  |   _$$SnPostCategoryImplCopyWith<_$SnPostCategoryImpl> get copyWith => | ||||||
|  |       __$$SnPostCategoryImplCopyWithImpl<_$SnPostCategoryImpl>( | ||||||
|  |           this, _$identity); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Map<String, dynamic> toJson() { | ||||||
|  |     return _$$SnPostCategoryImplToJson( | ||||||
|  |       this, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | abstract class _SnPostCategory implements SnPostCategory { | ||||||
|  |   const factory _SnPostCategory( | ||||||
|  |       {required final int id, | ||||||
|  |       required final DateTime createdAt, | ||||||
|  |       required final DateTime updatedAt, | ||||||
|  |       required final dynamic deletedAt, | ||||||
|  |       required final String alias, | ||||||
|  |       required final String name, | ||||||
|  |       required final String description, | ||||||
|  |       required final dynamic posts}) = _$SnPostCategoryImpl; | ||||||
|  |  | ||||||
|  |   factory _SnPostCategory.fromJson(Map<String, dynamic> json) = | ||||||
|  |       _$SnPostCategoryImpl.fromJson; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   int get id; | ||||||
|  |   @override | ||||||
|  |   DateTime get createdAt; | ||||||
|  |   @override | ||||||
|  |   DateTime get updatedAt; | ||||||
|  |   @override | ||||||
|  |   dynamic get deletedAt; | ||||||
|  |   @override | ||||||
|  |   String get alias; | ||||||
|  |   @override | ||||||
|  |   String get name; | ||||||
|  |   @override | ||||||
|  |   String get description; | ||||||
|  |   @override | ||||||
|  |   dynamic get posts; | ||||||
|  |  | ||||||
|  |   /// Create a copy of SnPostCategory | ||||||
|  |   /// with the given fields replaced by the non-null parameter values. | ||||||
|  |   @override | ||||||
|  |   @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  |   _$$SnPostCategoryImplCopyWith<_$SnPostCategoryImpl> get copyWith => | ||||||
|  |       throw _privateConstructorUsedError; | ||||||
|  | } | ||||||
|  |  | ||||||
| SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) { | SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) { | ||||||
|   return _SnPostPreload.fromJson(json); |   return _SnPostPreload.fromJson(json); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,7 +22,10 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl( | |||||||
|               ?.map((e) => SnPostTag.fromJson(e as Map<String, dynamic>)) |               ?.map((e) => SnPostTag.fromJson(e as Map<String, dynamic>)) | ||||||
|               .toList() ?? |               .toList() ?? | ||||||
|           const [], |           const [], | ||||||
|       categories: json['categories'] as List<dynamic>? ?? const [], |       categories: (json['categories'] as List<dynamic>?) | ||||||
|  |               ?.map((e) => SnPostCategory.fromJson(e as Map<String, dynamic>)) | ||||||
|  |               .toList() ?? | ||||||
|  |           const [], | ||||||
|       replies: (json['replies'] as List<dynamic>?) |       replies: (json['replies'] as List<dynamic>?) | ||||||
|           ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>)) |           ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>)) | ||||||
|           .toList(), |           .toList(), | ||||||
| @@ -80,7 +83,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) => | |||||||
|       'alias': instance.alias, |       'alias': instance.alias, | ||||||
|       'alias_prefix': instance.aliasPrefix, |       'alias_prefix': instance.aliasPrefix, | ||||||
|       'tags': instance.tags.map((e) => e.toJson()).toList(), |       'tags': instance.tags.map((e) => e.toJson()).toList(), | ||||||
|       'categories': instance.categories, |       'categories': instance.categories.map((e) => e.toJson()).toList(), | ||||||
|       'replies': instance.replies?.map((e) => e.toJson()).toList(), |       'replies': instance.replies?.map((e) => e.toJson()).toList(), | ||||||
|       'reply_id': instance.replyId, |       'reply_id': instance.replyId, | ||||||
|       'repost_id': instance.repostId, |       'repost_id': instance.repostId, | ||||||
| @@ -127,6 +130,31 @@ Map<String, dynamic> _$$SnPostTagImplToJson(_$SnPostTagImpl instance) => | |||||||
|       'posts': instance.posts, |       'posts': instance.posts, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  | _$SnPostCategoryImpl _$$SnPostCategoryImplFromJson(Map<String, dynamic> json) => | ||||||
|  |     _$SnPostCategoryImpl( | ||||||
|  |       id: (json['id'] as num).toInt(), | ||||||
|  |       createdAt: DateTime.parse(json['created_at'] as String), | ||||||
|  |       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||||
|  |       deletedAt: json['deleted_at'], | ||||||
|  |       alias: json['alias'] as String, | ||||||
|  |       name: json['name'] as String, | ||||||
|  |       description: json['description'] as String, | ||||||
|  |       posts: json['posts'], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | Map<String, dynamic> _$$SnPostCategoryImplToJson( | ||||||
|  |         _$SnPostCategoryImpl instance) => | ||||||
|  |     <String, dynamic>{ | ||||||
|  |       'id': instance.id, | ||||||
|  |       'created_at': instance.createdAt.toIso8601String(), | ||||||
|  |       'updated_at': instance.updatedAt.toIso8601String(), | ||||||
|  |       'deleted_at': instance.deletedAt, | ||||||
|  |       'alias': instance.alias, | ||||||
|  |       'name': instance.name, | ||||||
|  |       'description': instance.description, | ||||||
|  |       'posts': instance.posts, | ||||||
|  |     }; | ||||||
|  |  | ||||||
| _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) => | _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) => | ||||||
|     _$SnPostPreloadImpl( |     _$SnPostPreloadImpl( | ||||||
|       thumbnail: json['thumbnail'] == null |       thumbnail: json['thumbnail'] == null | ||||||
|   | |||||||
| @@ -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:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:livekit_client/livekit_client.dart'; | import 'package:livekit_client/livekit_client.dart'; | ||||||
| @@ -130,7 +130,7 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> { | |||||||
|                     Text('callCamera').tr(), |                     Text('callCamera').tr(), | ||||||
|                     Switch( |                     Switch( | ||||||
|                       value: call.enableVideo, |                       value: call.enableVideo, | ||||||
|                       onChanged: (value) => call.setEnableAudio(value), |                       onChanged: call.setEnableVideo, | ||||||
|                     ), |                     ), | ||||||
|                   ], |                   ], | ||||||
|                 ).padding(bottom: 5), |                 ).padding(bottom: 5), | ||||||
|   | |||||||
| @@ -142,7 +142,7 @@ class ChatMessage extends StatelessWidget { | |||||||
|                             onEdit: onEdit, |                             onEdit: onEdit, | ||||||
|                             onDelete: onDelete, |                             onDelete: onDelete, | ||||||
|                           ), |                           ), | ||||||
|                         )).padding(bottom: 4, top: isMerged ? 4 : 2), |                         )).padding(bottom: 4, top: 4), | ||||||
|                       switch (data.type) { |                       switch (data.type) { | ||||||
|                         'messages.new' => _ChatMessageText(data: data), |                         'messages.new' => _ChatMessageText(data: data), | ||||||
|                         _ => _ChatMessageSystemNotify(data: data), |                         _ => _ChatMessageSystemNotify(data: data), | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; |  | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:responsive_framework/responsive_framework.dart'; | import 'package:responsive_framework/responsive_framework.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|   | |||||||
| @@ -17,6 +17,8 @@ import 'package:responsive_framework/responsive_framework.dart'; | |||||||
| import 'package:screenshot/screenshot.dart'; | import 'package:screenshot/screenshot.dart'; | ||||||
| import 'package:share_plus/share_plus.dart'; | import 'package:share_plus/share_plus.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
|  | import 'package:surface/providers/link_preview.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| @@ -83,6 +85,8 @@ class PostItem extends StatelessWidget { | |||||||
|             child: MultiProvider( |             child: MultiProvider( | ||||||
|               providers: [ |               providers: [ | ||||||
|                 Provider<SnNetworkProvider>(create: (_) => context.read()), |                 Provider<SnNetworkProvider>(create: (_) => context.read()), | ||||||
|  |                 Provider<SnLinkPreviewProvider>(create: (_) => context.read()), | ||||||
|  |                 ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()), | ||||||
|               ], |               ], | ||||||
|               child: ResponsiveBreakpoints.builder( |               child: ResponsiveBreakpoints.builder( | ||||||
|                 breakpoints: ResponsiveBreakpoints.of(context).breakpoints, |                 breakpoints: ResponsiveBreakpoints.of(context).breakpoints, | ||||||
| @@ -175,6 +179,7 @@ class PostItem extends StatelessWidget { | |||||||
|                     children: [ |                     children: [ | ||||||
|                       if (data.visibility > 0) _PostVisibilityHint(data: data), |                       if (data.visibility > 0) _PostVisibilityHint(data: data), | ||||||
|                       _PostTruncatedHint(data: data), |                       _PostTruncatedHint(data: data), | ||||||
|  |                       if (data.tags.isNotEmpty) _PostTagsList(data: data), | ||||||
|                     ], |                     ], | ||||||
|                   ).padding(horizontal: 12), |                   ).padding(horizontal: 12), | ||||||
|                   const Gap(8), |                   const Gap(8), | ||||||
| @@ -182,7 +187,6 @@ class PostItem extends StatelessWidget { | |||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), |             Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), | ||||||
|             if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, bottom: 6), |  | ||||||
|             _PostBottomAction( |             _PostBottomAction( | ||||||
|               data: data, |               data: data, | ||||||
|               showComments: showComments, |               showComments: showComments, | ||||||
| @@ -241,7 +245,7 @@ class PostItem extends StatelessWidget { | |||||||
|                   horizontal: 16, |                   horizontal: 16, | ||||||
|                   vertical: 4, |                   vertical: 4, | ||||||
|                 ), |                 ), | ||||||
|               if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, bottom: 6), |               if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
| @@ -410,7 +414,7 @@ class PostShareImageWidget extends StatelessWidget { | |||||||
|                     size: Size(28, 28), |                     size: Size(28, 28), | ||||||
|                   ), |                   ), | ||||||
|                   eyeStyle: QrEyeStyle( |                   eyeStyle: QrEyeStyle( | ||||||
|                     eyeShape: QrEyeShape.square, |                     eyeShape: QrEyeShape.circle, | ||||||
|                     color: Theme.of(context).colorScheme.onSurface, |                     color: Theme.of(context).colorScheme.onSurface, | ||||||
|                   ), |                   ), | ||||||
|                   dataModuleStyle: QrDataModuleStyle( |                   dataModuleStyle: QrDataModuleStyle( | ||||||
| @@ -458,6 +462,7 @@ class _PostBottomAction extends StatelessWidget { | |||||||
|       children: [ |       children: [ | ||||||
|         if (showReactions || showComments) |         if (showReactions || showComments) | ||||||
|           Row( |           Row( | ||||||
|  |             spacing: 8, | ||||||
|             children: [ |             children: [ | ||||||
|               if (showReactions) |               if (showReactions) | ||||||
|                 InkWell( |                 InkWell( | ||||||
| @@ -523,8 +528,7 @@ class _PostBottomAction extends StatelessWidget { | |||||||
|                     ); |                     ); | ||||||
|                   }, |                   }, | ||||||
|                 ), |                 ), | ||||||
|             ].expand((ele) => [ele, const Gap(8)]).toList() |             ], | ||||||
|               ..removeLast(), |  | ||||||
|           ), |           ), | ||||||
|         InkWell( |         InkWell( | ||||||
|           onTap: onShare, |           onTap: onShare, | ||||||
| @@ -962,23 +966,69 @@ class _PostTagsList extends StatelessWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Wrap( |     return Column( | ||||||
|       spacing: 4, |       mainAxisSize: MainAxisSize.min, | ||||||
|       runSpacing: 4, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|       children: data.tags |       children: [ | ||||||
|           .map( |         Wrap( | ||||||
|             (ele) => InkWell( |           spacing: 4, | ||||||
|               child: Text( |           runSpacing: 4, | ||||||
|                 '#${ele.alias}', |           children: data.categories | ||||||
|                 style: TextStyle( |               .map( | ||||||
|                   decoration: TextDecoration.underline, |                 (ele) => InkWell( | ||||||
|  |                   child: Row( | ||||||
|  |                     mainAxisSize: MainAxisSize.min, | ||||||
|  |                     children: [ | ||||||
|  |                       const Icon(Symbols.category, size: 20), | ||||||
|  |                       const Gap(4), | ||||||
|  |                       Text( | ||||||
|  |                         'postCategory${ele.alias.capitalize()}'.trExists() | ||||||
|  |                             ? 'postCategory${ele.alias.capitalize()}'.tr() | ||||||
|  |                             : ele.alias, | ||||||
|  |                         style: GoogleFonts.robotoMono(), | ||||||
|  |                       ), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                   onTap: () { | ||||||
|  |                     GoRouter.of(context).pushNamed( | ||||||
|  |                       'postSearch', | ||||||
|  |                       queryParameters: { | ||||||
|  |                         'categories': ele.alias, | ||||||
|  |                       }, | ||||||
|  |                     ); | ||||||
|  |                   }, | ||||||
|                 ), |                 ), | ||||||
|               ).fontSize(13), |               ) | ||||||
|               onTap: () {}, |               .toList(), | ||||||
|             ), |         ).opacity(0.8), | ||||||
|           ) |         Wrap( | ||||||
|           .toList(), |           spacing: 4, | ||||||
|     ).opacity(0.8); |           runSpacing: 4, | ||||||
|  |           children: data.tags | ||||||
|  |               .map( | ||||||
|  |                 (ele) => InkWell( | ||||||
|  |                   child: Row( | ||||||
|  |                     mainAxisSize: MainAxisSize.min, | ||||||
|  |                     children: [ | ||||||
|  |                       const Icon(Symbols.label, size: 20), | ||||||
|  |                       const Gap(4), | ||||||
|  |                       Text(ele.alias, style: GoogleFonts.robotoMono()), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                   onTap: () { | ||||||
|  |                     GoRouter.of(context).pushNamed( | ||||||
|  |                       'postSearch', | ||||||
|  |                       queryParameters: { | ||||||
|  |                         'tags': ele.alias, | ||||||
|  |                       }, | ||||||
|  |                     ); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|  |               ) | ||||||
|  |               .toList(), | ||||||
|  |         ).opacity(0.8), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1019,6 +1069,7 @@ class _PostTruncatedHint extends StatelessWidget { | |||||||
|     return SingleChildScrollView( |     return SingleChildScrollView( | ||||||
|       scrollDirection: Axis.horizontal, |       scrollDirection: Axis.horizontal, | ||||||
|       child: Row( |       child: Row( | ||||||
|  |         spacing: 8, | ||||||
|         children: [ |         children: [ | ||||||
|           if (data.body['content_length'] != null) |           if (data.body['content_length'] != null) | ||||||
|             Row( |             Row( | ||||||
| @@ -1031,7 +1082,7 @@ class _PostTruncatedHint extends StatelessWidget { | |||||||
|                   ).inSeconds}s', |                   ).inSeconds}s', | ||||||
|                 ]), |                 ]), | ||||||
|               ], |               ], | ||||||
|             ).padding(right: 8), |             ), | ||||||
|           if (data.body['content_length'] != null) |           if (data.body['content_length'] != null) | ||||||
|             Row( |             Row( | ||||||
|               children: [ |               children: [ | ||||||
|   | |||||||
| @@ -189,16 +189,19 @@ class PostMediaPendingList extends StatelessWidget { | |||||||
|                   child: AspectRatio( |                   child: AspectRatio( | ||||||
|                     aspectRatio: 1, |                     aspectRatio: 1, | ||||||
|                     child: switch (thumbnail!.type) { |                     child: switch (thumbnail!.type) { | ||||||
|                       PostWriteMediaType.image => LayoutBuilder(builder: (context, constraints) { |                       PostWriteMediaType.image => Container( | ||||||
|                           return Image( |                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|                             image: thumbnail!.getImageProvider( |                         child: LayoutBuilder(builder: (context, constraints) { | ||||||
|                               context, |                             return Image( | ||||||
|                               width: (constraints.maxWidth * devicePixelRatio).round(), |                               image: thumbnail!.getImageProvider( | ||||||
|                               height: (constraints.maxHeight * devicePixelRatio).round(), |                                 context, | ||||||
|                             )!, |                                 width: (constraints.maxWidth * devicePixelRatio).round(), | ||||||
|                             fit: BoxFit.cover, |                                 height: (constraints.maxHeight * devicePixelRatio).round(), | ||||||
|                           ); |                               )!, | ||||||
|                         }), |                               fit: BoxFit.contain, | ||||||
|  |                             ); | ||||||
|  |                           }), | ||||||
|  |                       ), | ||||||
|                       _ => Container( |                       _ => Container( | ||||||
|                           color: Theme.of(context).colorScheme.surface, |                           color: Theme.of(context).colorScheme.surface, | ||||||
|                           child: const Icon(Symbols.docs).center(), |                           child: const Icon(Symbols.docs).center(), | ||||||
| @@ -236,18 +239,21 @@ class PostMediaPendingList extends StatelessWidget { | |||||||
|                       child: AspectRatio( |                       child: AspectRatio( | ||||||
|                         aspectRatio: 1, |                         aspectRatio: 1, | ||||||
|                         child: switch (media.type) { |                         child: switch (media.type) { | ||||||
|                           PostWriteMediaType.image => LayoutBuilder(builder: (context, constraints) { |                           PostWriteMediaType.image => Container( | ||||||
|                               return Image( |                             color: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|                                 image: media.getImageProvider( |                             child: LayoutBuilder(builder: (context, constraints) { | ||||||
|                                   context, |                                 return Image( | ||||||
|                                   width: (constraints.maxWidth * devicePixelRatio).round(), |                                   image: media.getImageProvider( | ||||||
|                                   height: (constraints.maxHeight * devicePixelRatio).round(), |                                     context, | ||||||
|                                 )!, |                                     width: (constraints.maxWidth * devicePixelRatio).round(), | ||||||
|                                 fit: BoxFit.cover, |                                     height: (constraints.maxHeight * devicePixelRatio).round(), | ||||||
|                               ); |                                   )!, | ||||||
|                             }), |                                   fit: BoxFit.contain, | ||||||
|  |                                 ); | ||||||
|  |                               }), | ||||||
|  |                           ), | ||||||
|                           _ => Container( |                           _ => Container( | ||||||
|                               color: Theme.of(context).colorScheme.surface, |                               color: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|                               child: const Icon(Symbols.docs).center(), |                               child: const Icon(Symbols.docs).center(), | ||||||
|                             ), |                             ), | ||||||
|                         }, |                         }, | ||||||
|   | |||||||
| @@ -83,155 +83,178 @@ class PostMetaEditor extends StatelessWidget { | |||||||
|     return ListenableBuilder( |     return ListenableBuilder( | ||||||
|       listenable: controller, |       listenable: controller, | ||||||
|       builder: (context, _) { |       builder: (context, _) { | ||||||
|         return Column( |         return SingleChildScrollView( | ||||||
|           children: [ |           padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8), | ||||||
|             TextField( |           child: Column( | ||||||
|               controller: controller.titleController, |             children: [ | ||||||
|               decoration: InputDecoration( |  | ||||||
|                 labelText: 'fieldPostTitle'.tr(), |  | ||||||
|                 border: UnderlineInputBorder(), |  | ||||||
|               ), |  | ||||||
|               onTapOutside: (_) => |  | ||||||
|                   FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|             ).padding(horizontal: 24), |  | ||||||
|             if (controller.mode == 'articles') const Gap(4), |  | ||||||
|             if (controller.mode == 'articles') |  | ||||||
|               TextField( |               TextField( | ||||||
|                 controller: controller.descriptionController, |                 controller: controller.titleController, | ||||||
|                 maxLines: null, |  | ||||||
|                 decoration: InputDecoration( |                 decoration: InputDecoration( | ||||||
|                   labelText: 'fieldPostDescription'.tr(), |                   labelText: 'fieldPostTitle'.tr(), | ||||||
|                   border: UnderlineInputBorder(), |                   border: UnderlineInputBorder(), | ||||||
|                 ), |                 ), | ||||||
|                 onTapOutside: (_) => |                 onTapOutside: (_) => | ||||||
|                     FocusManager.instance.primaryFocus?.unfocus(), |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|               ).padding(horizontal: 24), |               ).padding(horizontal: 24), | ||||||
|             const Gap(4), |               if (controller.mode == 'articles') const Gap(4), | ||||||
|             PostTagsField( |               if (controller.mode == 'articles') | ||||||
|               initialTags: controller.tags, |                 TextField( | ||||||
|               labelText: 'fieldPostTags'.tr(), |                   controller: controller.descriptionController, | ||||||
|               onUpdate: (value) { |                   maxLines: null, | ||||||
|                 controller.setTags(value); |                   decoration: InputDecoration( | ||||||
|               }, |                     labelText: 'fieldPostDescription'.tr(), | ||||||
|             ).padding(horizontal: 24), |                     border: UnderlineInputBorder(), | ||||||
|             const Gap(12), |                   ), | ||||||
|             ListTile( |                   onTapOutside: (_) => | ||||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), |                       FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|               leading: const Icon(Symbols.visibility), |                 ).padding(horizontal: 24), | ||||||
|               title: Text('postVisibility').tr(), |               const Gap(4), | ||||||
|               subtitle: Text('postVisibilityDescription').tr(), |               PostTagsField( | ||||||
|               trailing: SizedBox( |                 initialTags: controller.tags, | ||||||
|                 width: 180, |                 labelText: 'fieldPostTags'.tr(), | ||||||
|                 child: DropdownButtonHideUnderline( |                 onUpdate: (value) { | ||||||
|                   child: DropdownButton2<int>( |                   controller.setTags(value); | ||||||
|                     isExpanded: true, |                 }, | ||||||
|                     items: kPostVisibilityLevel.entries |               ).padding(horizontal: 24), | ||||||
|                         .map( |               const Gap(4), | ||||||
|                           (entry) => DropdownMenuItem<int>( |               PostCategoriesField( | ||||||
|                             value: entry.key, |                 initialCategories: controller.categories, | ||||||
|                             child: Text( |                 labelText: 'fieldPostCategories'.tr(), | ||||||
|                               entry.value, |                 onUpdate: (value) { | ||||||
|                               style: const TextStyle(fontSize: 14), |                   controller.setCategories(value); | ||||||
|                             ).tr(), |                 }, | ||||||
|                           ), |               ).padding(horizontal: 24), | ||||||
|                         ) |               const Gap(4), | ||||||
|                         .toList(), |               TextField( | ||||||
|                     value: controller.visibility, |                 controller: controller.aliasController, | ||||||
|                     onChanged: (int? value) { |                 decoration: InputDecoration( | ||||||
|                       if (value != null) { |                   labelText: 'fieldPostAlias'.tr(), | ||||||
|                         controller.setVisibility(value); |                   helperText: 'fieldPostAliasHint'.tr(), | ||||||
|                       } |                   helperMaxLines: 2, | ||||||
|                     }, |                   border: UnderlineInputBorder(), | ||||||
|                     buttonStyleData: const ButtonStyleData( |                 ), | ||||||
|                       height: 40, |                 onTapOutside: (_) => | ||||||
|                       padding: EdgeInsets.symmetric( |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                         horizontal: 4, |               ).padding(horizontal: 24), | ||||||
|                         vertical: 8, |               const Gap(12), | ||||||
|  |               ListTile( | ||||||
|  |                 contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |                 leading: const Icon(Symbols.visibility), | ||||||
|  |                 title: Text('postVisibility').tr(), | ||||||
|  |                 subtitle: Text('postVisibilityDescription').tr(), | ||||||
|  |                 trailing: SizedBox( | ||||||
|  |                   width: 180, | ||||||
|  |                   child: DropdownButtonHideUnderline( | ||||||
|  |                     child: DropdownButton2<int>( | ||||||
|  |                       isExpanded: true, | ||||||
|  |                       items: kPostVisibilityLevel.entries | ||||||
|  |                           .map( | ||||||
|  |                             (entry) => DropdownMenuItem<int>( | ||||||
|  |                               value: entry.key, | ||||||
|  |                               child: Text( | ||||||
|  |                                 entry.value, | ||||||
|  |                                 style: const TextStyle(fontSize: 14), | ||||||
|  |                               ).tr(), | ||||||
|  |                             ), | ||||||
|  |                           ) | ||||||
|  |                           .toList(), | ||||||
|  |                       value: controller.visibility, | ||||||
|  |                       onChanged: (int? value) { | ||||||
|  |                         if (value != null) { | ||||||
|  |                           controller.setVisibility(value); | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                       buttonStyleData: const ButtonStyleData( | ||||||
|  |                         height: 40, | ||||||
|  |                         padding: EdgeInsets.symmetric( | ||||||
|  |                           horizontal: 4, | ||||||
|  |                           vertical: 8, | ||||||
|  |                         ), | ||||||
|                       ), |                       ), | ||||||
|  |                       menuItemStyleData: const MenuItemStyleData(height: 40), | ||||||
|                     ), |                     ), | ||||||
|                     menuItemStyleData: const MenuItemStyleData(height: 40), |  | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |               if (controller.visibility == 2) | ||||||
|             if (controller.visibility == 2) |                 ListTile( | ||||||
|  |                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |                   leading: Icon(Symbols.person), | ||||||
|  |                   trailing: Icon(Symbols.chevron_right), | ||||||
|  |                   title: Text('postVisibleUsers').tr(), | ||||||
|  |                   subtitle: Text('postSelectedUsers') | ||||||
|  |                       .plural(controller.visibleUsers.length), | ||||||
|  |                   onTap: () { | ||||||
|  |                     _selectVisibleUser(context); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|  |               if (controller.visibility == 3) | ||||||
|  |                 ListTile( | ||||||
|  |                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |                   leading: Icon(Symbols.person), | ||||||
|  |                   trailing: Icon(Symbols.chevron_right), | ||||||
|  |                   title: Text('postInvisibleUsers').tr(), | ||||||
|  |                   subtitle: Text('postSelectedUsers') | ||||||
|  |                       .plural(controller.invisibleUsers.length), | ||||||
|  |                   onTap: () { | ||||||
|  |                     _selectInvisibleUser(context); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|               ListTile( |               ListTile( | ||||||
|                 contentPadding: const EdgeInsets.symmetric(horizontal: 24), |                 leading: const Icon(Symbols.event_available), | ||||||
|                 leading: Icon(Symbols.person), |                 title: Text('postPublishedAt').tr(), | ||||||
|                 trailing: Icon(Symbols.chevron_right), |                 subtitle: Text( | ||||||
|                 title: Text('postVisibleUsers').tr(), |                   controller.publishedAt != null | ||||||
|                 subtitle: Text('postSelectedUsers') |                       ? dateFormatter.format(controller.publishedAt!) | ||||||
|                     .plural(controller.visibleUsers.length), |                       : 'unset'.tr(), | ||||||
|  |                 ), | ||||||
|  |                 trailing: controller.publishedAt != null | ||||||
|  |                     ? IconButton( | ||||||
|  |                         icon: const Icon(Symbols.cancel), | ||||||
|  |                         onPressed: () { | ||||||
|  |                           controller.setPublishedAt(null); | ||||||
|  |                         }, | ||||||
|  |                       ) | ||||||
|  |                     : null, | ||||||
|  |                 contentPadding: const EdgeInsets.only(left: 24, right: 18), | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   _selectVisibleUser(context); |                   _selectDate( | ||||||
|  |                     context, | ||||||
|  |                     initialDateTime: controller.publishedAt, | ||||||
|  |                   ).then((value) { | ||||||
|  |                     controller.setPublishedAt(value); | ||||||
|  |                   }); | ||||||
|                 }, |                 }, | ||||||
|               ), |               ), | ||||||
|             if (controller.visibility == 3) |  | ||||||
|               ListTile( |               ListTile( | ||||||
|                 contentPadding: const EdgeInsets.symmetric(horizontal: 24), |                 leading: const Icon(Symbols.event_busy), | ||||||
|                 leading: Icon(Symbols.person), |                 title: Text('postPublishedUntil').tr(), | ||||||
|                 trailing: Icon(Symbols.chevron_right), |                 subtitle: Text( | ||||||
|                 title: Text('postInvisibleUsers').tr(), |                   controller.publishedUntil != null | ||||||
|                 subtitle: Text('postSelectedUsers') |                       ? dateFormatter.format(controller.publishedUntil!) | ||||||
|                     .plural(controller.invisibleUsers.length), |                       : 'unset'.tr(), | ||||||
|  |                 ), | ||||||
|  |                 trailing: controller.publishedUntil != null | ||||||
|  |                     ? IconButton( | ||||||
|  |                         icon: const Icon(Symbols.cancel), | ||||||
|  |                         onPressed: () { | ||||||
|  |                           controller.setPublishedUntil(null); | ||||||
|  |                         }, | ||||||
|  |                       ) | ||||||
|  |                     : null, | ||||||
|  |                 contentPadding: const EdgeInsets.only(left: 24, right: 18), | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   _selectInvisibleUser(context); |                   _selectDate( | ||||||
|  |                     context, | ||||||
|  |                     initialDateTime: controller.publishedUntil, | ||||||
|  |                   ).then((value) { | ||||||
|  |                     controller.setPublishedUntil(value); | ||||||
|  |                   }); | ||||||
|                 }, |                 }, | ||||||
|               ), |               ), | ||||||
|             ListTile( |             ], | ||||||
|               leading: const Icon(Symbols.event_available), |           ).padding(vertical: 8), | ||||||
|               title: Text('postPublishedAt').tr(), |         ); | ||||||
|               subtitle: Text( |  | ||||||
|                 controller.publishedAt != null |  | ||||||
|                     ? dateFormatter.format(controller.publishedAt!) |  | ||||||
|                     : 'unset'.tr(), |  | ||||||
|               ), |  | ||||||
|               trailing: controller.publishedAt != null |  | ||||||
|                   ? IconButton( |  | ||||||
|                       icon: const Icon(Symbols.cancel), |  | ||||||
|                       onPressed: () { |  | ||||||
|                         controller.setPublishedAt(null); |  | ||||||
|                       }, |  | ||||||
|                     ) |  | ||||||
|                   : null, |  | ||||||
|               contentPadding: const EdgeInsets.only(left: 24, right: 18), |  | ||||||
|               onTap: () { |  | ||||||
|                 _selectDate( |  | ||||||
|                   context, |  | ||||||
|                   initialDateTime: controller.publishedAt, |  | ||||||
|                 ).then((value) { |  | ||||||
|                   controller.setPublishedAt(value); |  | ||||||
|                 }); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             ListTile( |  | ||||||
|               leading: const Icon(Symbols.event_busy), |  | ||||||
|               title: Text('postPublishedUntil').tr(), |  | ||||||
|               subtitle: Text( |  | ||||||
|                 controller.publishedUntil != null |  | ||||||
|                     ? dateFormatter.format(controller.publishedUntil!) |  | ||||||
|                     : 'unset'.tr(), |  | ||||||
|               ), |  | ||||||
|               trailing: controller.publishedUntil != null |  | ||||||
|                   ? IconButton( |  | ||||||
|                       icon: const Icon(Symbols.cancel), |  | ||||||
|                       onPressed: () { |  | ||||||
|                         controller.setPublishedUntil(null); |  | ||||||
|                       }, |  | ||||||
|                     ) |  | ||||||
|                   : null, |  | ||||||
|               contentPadding: const EdgeInsets.only(left: 24, right: 18), |  | ||||||
|               onTap: () { |  | ||||||
|                 _selectDate( |  | ||||||
|                   context, |  | ||||||
|                   initialDateTime: controller.publishedUntil, |  | ||||||
|                 ).then((value) { |  | ||||||
|                   controller.setPublishedUntil(value); |  | ||||||
|                 }); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|           ], |  | ||||||
|         ).padding(vertical: 8); |  | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/controllers/post_write_controller.dart'; | import 'package:surface/controllers/post_write_controller.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| @@ -16,6 +17,7 @@ import 'package:surface/widgets/loading_indicator.dart'; | |||||||
| class PostMiniEditor extends StatefulWidget { | class PostMiniEditor extends StatefulWidget { | ||||||
|   final int? postReplyId; |   final int? postReplyId; | ||||||
|   final Function? onPost; |   final Function? onPost; | ||||||
|  |  | ||||||
|   const PostMiniEditor({super.key, this.postReplyId, this.onPost}); |   const PostMiniEditor({super.key, this.postReplyId, this.onPost}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -26,6 +28,7 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|   final PostWriteController _writeController = PostWriteController(); |   final PostWriteController _writeController = PostWriteController(); | ||||||
|  |  | ||||||
|   bool _isFetching = false; |   bool _isFetching = false; | ||||||
|  |  | ||||||
|   bool get _isLoading => _isFetching || _writeController.isLoading; |   bool get _isLoading => _isFetching || _writeController.isLoading; | ||||||
|  |  | ||||||
|   List<SnPublisher>? _publishers; |   List<SnPublisher>? _publishers; | ||||||
| @@ -35,11 +38,14 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final sn = context.read<SnNetworkProvider>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final config = context.read<ConfigProvider>(); | ||||||
|       final resp = await sn.client.get('/cgi/co/publishers/me'); |       final resp = await sn.client.get('/cgi/co/publishers/me'); | ||||||
|       _publishers = List<SnPublisher>.from( |       _publishers = List<SnPublisher>.from( | ||||||
|         resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], |         resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], | ||||||
|       ); |       ); | ||||||
|       _writeController.setPublisher(_publishers?.firstOrNull); |       final beforeId = config.prefs.getInt('int_last_publisher_id'); | ||||||
|  |       _writeController | ||||||
|  |           .setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       context.showErrorDialog(err); |       context.showErrorDialog(err); | ||||||
| @@ -93,17 +99,11 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                                 Expanded( |                                 Expanded( | ||||||
|                                   child: Column( |                                   child: Column( | ||||||
|                                     mainAxisSize: MainAxisSize.min, |                                     mainAxisSize: MainAxisSize.min, | ||||||
|                                     crossAxisAlignment: |                                     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                                         CrossAxisAlignment.start, |  | ||||||
|                                     children: [ |                                     children: [ | ||||||
|                                       Text(item.nick).textStyle( |                                       Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!), | ||||||
|                                           Theme.of(context) |  | ||||||
|                                               .textTheme |  | ||||||
|                                               .bodyMedium!), |  | ||||||
|                                       Text('@${item.name}') |                                       Text('@${item.name}') | ||||||
|                                           .textStyle(Theme.of(context) |                                           .textStyle(Theme.of(context).textTheme.bodySmall!) | ||||||
|                                               .textTheme |  | ||||||
|                                               .bodySmall!) |  | ||||||
|                                           .fontSize(12), |                                           .fontSize(12), | ||||||
|                                     ], |                                     ], | ||||||
|                                   ), |                                   ), | ||||||
| @@ -120,8 +120,7 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                           CircleAvatar( |                           CircleAvatar( | ||||||
|                             radius: 16, |                             radius: 16, | ||||||
|                             backgroundColor: Colors.transparent, |                             backgroundColor: Colors.transparent, | ||||||
|                             foregroundColor: |                             foregroundColor: Theme.of(context).colorScheme.onSurface, | ||||||
|                                 Theme.of(context).colorScheme.onSurface, |  | ||||||
|                             child: const Icon(Symbols.add), |                             child: const Icon(Symbols.add), | ||||||
|                           ), |                           ), | ||||||
|                           const Gap(8), |                           const Gap(8), | ||||||
| @@ -130,8 +129,7 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                               mainAxisSize: MainAxisSize.min, |                               mainAxisSize: MainAxisSize.min, | ||||||
|                               crossAxisAlignment: CrossAxisAlignment.start, |                               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                               children: [ |                               children: [ | ||||||
|                                 Text('publishersNew').tr().textStyle( |                                 Text('publishersNew').tr().textStyle(Theme.of(context).textTheme.bodyMedium!), | ||||||
|                                     Theme.of(context).textTheme.bodyMedium!), |  | ||||||
|                               ], |                               ], | ||||||
|                             ), |                             ), | ||||||
|                           ), |                           ), | ||||||
| @@ -142,9 +140,7 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                   value: _writeController.publisher, |                   value: _writeController.publisher, | ||||||
|                   onChanged: (SnPublisher? value) { |                   onChanged: (SnPublisher? value) { | ||||||
|                     if (value == null) { |                     if (value == null) { | ||||||
|                       GoRouter.of(context) |                       GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { | ||||||
|                           .pushNamed('accountPublisherNew') |  | ||||||
|                           .then((value) { |  | ||||||
|                         if (value == true) { |                         if (value == true) { | ||||||
|                           _publishers = null; |                           _publishers = null; | ||||||
|                           _fetchPublishers(); |                           _fetchPublishers(); | ||||||
| @@ -152,6 +148,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                       }); |                       }); | ||||||
|                     } else { |                     } else { | ||||||
|                       _writeController.setPublisher(value); |                       _writeController.setPublisher(value); | ||||||
|  |                       final config = context.read<ConfigProvider>(); | ||||||
|  |                       config.prefs.setInt('int_last_publisher_id', value.id); | ||||||
|                     } |                     } | ||||||
|                   }, |                   }, | ||||||
|                   buttonStyleData: const ButtonStyleData( |                   buttonStyleData: const ButtonStyleData( | ||||||
| @@ -178,8 +176,7 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                     ), |                     ), | ||||||
|                     border: InputBorder.none, |                     border: InputBorder.none, | ||||||
|                   ), |                   ), | ||||||
|                   onTapOutside: (_) => |                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                       FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|               const Gap(8), |               const Gap(8), | ||||||
| @@ -188,8 +185,7 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                 TweenAnimationBuilder<double>( |                 TweenAnimationBuilder<double>( | ||||||
|                   tween: Tween(begin: 0, end: _writeController.progress), |                   tween: Tween(begin: 0, end: _writeController.progress), | ||||||
|                   duration: Duration(milliseconds: 300), |                   duration: Duration(milliseconds: 300), | ||||||
|                   builder: (context, value, _) => |                   builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), | ||||||
|                       LinearProgressIndicator(value: value, minHeight: 2), |  | ||||||
|                 ) |                 ) | ||||||
|               else if (_writeController.isBusy) |               else if (_writeController.isBusy) | ||||||
|                 const LinearProgressIndicator(value: null, minHeight: 2), |                 const LinearProgressIndicator(value: null, minHeight: 2), | ||||||
| @@ -206,18 +202,16 @@ class _PostMiniEditorState extends State<PostMiniEditor> { | |||||||
|                         'postEditor', |                         'postEditor', | ||||||
|                         pathParameters: {'mode': 'stories'}, |                         pathParameters: {'mode': 'stories'}, | ||||||
|                         queryParameters: { |                         queryParameters: { | ||||||
|                           if (widget.postReplyId != null) |                           if (widget.postReplyId != null) 'replying': widget.postReplyId.toString(), | ||||||
|                             'replying': widget.postReplyId.toString(), |  | ||||||
|                         }, |                         }, | ||||||
|                       ); |                       ); | ||||||
|                     }, |                     }, | ||||||
|                   ), |                   ), | ||||||
|                   TextButton.icon( |                   TextButton.icon( | ||||||
|                     onPressed: (_writeController.isBusy || |                     onPressed: (_writeController.isBusy || _writeController.publisher == null) | ||||||
|                             _writeController.publisher == null) |  | ||||||
|                         ? null |                         ? null | ||||||
|                         : () { |                         : () { | ||||||
|                             _writeController.post(context).then((_) { |                             _writeController.sendPost(context).then((_) { | ||||||
|                               if (!context.mounted) return; |                               if (!context.mounted) return; | ||||||
|                               if (widget.onPost != null) widget.onPost!(); |                               if (widget.onPost != null) widget.onPost!(); | ||||||
|                               context.showSnackbar('postPosted'.tr()); |                               context.showSnackbar('postPosted'.tr()); | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
|  |  | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/widgets/dialog.dart'; | ||||||
|  |  | ||||||
| class PostTagsField extends StatefulWidget { | class PostTagsField extends StatefulWidget { | ||||||
|   final List<String>? initialTags; |   final List<String>? initialTags; | ||||||
| @@ -21,9 +23,9 @@ class PostTagsField extends StatefulWidget { | |||||||
|   State<PostTagsField> createState() => _PostTagsFieldState(); |   State<PostTagsField> createState() => _PostTagsFieldState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _PostTagsFieldState extends State<PostTagsField> { | const List<String> kTagsDividers = [' ', ',']; | ||||||
|   static const List<String> kTagsDividers = [' ', ',']; |  | ||||||
|  |  | ||||||
|  | class _PostTagsFieldState extends State<PostTagsField> { | ||||||
|   late final _Debounceable<List<String>?, String> _debouncedSearch; |   late final _Debounceable<List<String>?, String> _debouncedSearch; | ||||||
|  |  | ||||||
|   final List<String> _currentTags = List.empty(growable: true); |   final List<String> _currentTags = List.empty(growable: true); | ||||||
| @@ -100,8 +102,7 @@ class _PostTagsFieldState extends State<PostTagsField> { | |||||||
|                             color: Theme.of(context).colorScheme.primary, |                             color: Theme.of(context).colorScheme.primary, | ||||||
|                           ), |                           ), | ||||||
|                           margin: const EdgeInsets.only(right: 8), |                           margin: const EdgeInsets.only(right: 8), | ||||||
|                           padding: const EdgeInsets.symmetric( |                           padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), | ||||||
|                               horizontal: 10.0, vertical: 4.0), |  | ||||||
|                           child: Row( |                           child: Row( | ||||||
|                             mainAxisAlignment: MainAxisAlignment.spaceBetween, |                             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|                             children: [ |                             children: [ | ||||||
| @@ -155,6 +156,141 @@ class _PostTagsFieldState extends State<PostTagsField> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class PostCategoriesField extends StatefulWidget { | ||||||
|  |   final List<String>? initialCategories; | ||||||
|  |   final String labelText; | ||||||
|  |   final Function(List<String>) onUpdate; | ||||||
|  |  | ||||||
|  |   const PostCategoriesField({ | ||||||
|  |     super.key, | ||||||
|  |     this.initialCategories, | ||||||
|  |     required this.labelText, | ||||||
|  |     required this.onUpdate, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<PostCategoriesField> createState() => _PostCategoriesFieldState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PostCategoriesFieldState extends State<PostCategoriesField> { | ||||||
|  |   late final _Debounceable<List<String>?, String> _debouncedSearch; | ||||||
|  |  | ||||||
|  |   final List<String> _currentCategories = List.empty(growable: true); | ||||||
|  |  | ||||||
|  |   String? _currentSearchProbe; | ||||||
|  |   List<String> _lastAutocompleteResult = List.empty(); | ||||||
|  |   TextEditingController? _textEditingController; | ||||||
|  |  | ||||||
|  |   Future<List<String>?> _searchCategories(String probe) async { | ||||||
|  |     _currentSearchProbe = probe; | ||||||
|  |  | ||||||
|  |     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |     final resp = await sn.client.get( | ||||||
|  |       '/cgi/co/categories?take=10&probe=$_currentSearchProbe', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (_currentSearchProbe != probe) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     _currentSearchProbe = null; | ||||||
|  |  | ||||||
|  |     return resp.data.map((x) => x['alias']).toList().cast<String>(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _debouncedSearch = _debounce<List<String>?, String>(_searchCategories); | ||||||
|  |     if (widget.initialCategories != null) { | ||||||
|  |       _currentCategories.addAll(widget.initialCategories!); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Autocomplete<String>( | ||||||
|  |       optionsBuilder: (TextEditingValue textEditingValue) async { | ||||||
|  |         final result = await _debouncedSearch(textEditingValue.text); | ||||||
|  |         if (result == null) { | ||||||
|  |           return _lastAutocompleteResult; | ||||||
|  |         } | ||||||
|  |         _lastAutocompleteResult = result; | ||||||
|  |         return result; | ||||||
|  |       }, | ||||||
|  |       onSelected: (String value) { | ||||||
|  |         if (value.isEmpty) return; | ||||||
|  |         if (!_currentCategories.contains(value)) { | ||||||
|  |           setState(() => _currentCategories.add(value)); | ||||||
|  |         } | ||||||
|  |         _textEditingController?.clear(); | ||||||
|  |         widget.onUpdate(_currentCategories); | ||||||
|  |       }, | ||||||
|  |       fieldViewBuilder: (context, controller, focusNode, onSubmitted) { | ||||||
|  |         _textEditingController = controller; | ||||||
|  |         return TextField( | ||||||
|  |           controller: controller, | ||||||
|  |           focusNode: focusNode, | ||||||
|  |           decoration: InputDecoration( | ||||||
|  |             label: Text(widget.labelText), | ||||||
|  |             border: const UnderlineInputBorder(), | ||||||
|  |             prefixIconConstraints: BoxConstraints( | ||||||
|  |               maxWidth: MediaQuery.of(context).size.width * 0.75, | ||||||
|  |             ), | ||||||
|  |             prefixIcon: _currentCategories.isNotEmpty | ||||||
|  |                 ? SingleChildScrollView( | ||||||
|  |                     scrollDirection: Axis.horizontal, | ||||||
|  |                     child: Row( | ||||||
|  |                       children: _currentCategories.map((String category) { | ||||||
|  |                         return Container( | ||||||
|  |                           decoration: BoxDecoration( | ||||||
|  |                             borderRadius: const BorderRadius.all( | ||||||
|  |                               Radius.circular(20.0), | ||||||
|  |                             ), | ||||||
|  |                             color: Theme.of(context).colorScheme.primary, | ||||||
|  |                           ), | ||||||
|  |                           margin: const EdgeInsets.only(right: 8), | ||||||
|  |                           padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), | ||||||
|  |                           child: Row( | ||||||
|  |                             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                             children: [ | ||||||
|  |                               InkWell( | ||||||
|  |                                 child: Text( | ||||||
|  |                                   'postCategory${category.capitalize()}'.trExists() | ||||||
|  |                                       ? 'postCategory${category.capitalize()}'.tr() | ||||||
|  |                                       : '#$category', | ||||||
|  |                                   style: const TextStyle(color: Colors.white), | ||||||
|  |                                 ), | ||||||
|  |                               ), | ||||||
|  |                               const Gap(4), | ||||||
|  |                               InkWell( | ||||||
|  |                                 child: const Icon( | ||||||
|  |                                   Icons.cancel, | ||||||
|  |                                   size: 14.0, | ||||||
|  |                                   color: Color.fromARGB(255, 233, 233, 233), | ||||||
|  |                                 ), | ||||||
|  |                                 onTap: () { | ||||||
|  |                                   setState(() => _currentCategories.remove(category)); | ||||||
|  |                                   widget.onUpdate(_currentCategories); | ||||||
|  |                                 }, | ||||||
|  |                               ) | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|  |                         ); | ||||||
|  |                       }).toList(), | ||||||
|  |                     ), | ||||||
|  |                   ) | ||||||
|  |                 : null, | ||||||
|  |           ), | ||||||
|  |           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |           onSubmitted: (_) { | ||||||
|  |             onSubmitted(); | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| typedef _Debounceable<S, T> = Future<S?> Function(T parameter); | typedef _Debounceable<S, T> = Future<S?> Function(T parameter); | ||||||
|  |  | ||||||
| _Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) { | _Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) { | ||||||
|   | |||||||
| @@ -1,12 +1,15 @@ | |||||||
| import 'dart:io'; |  | ||||||
|  |  | ||||||
| import 'package:cached_network_image/cached_network_image.dart'; | import 'package:cached_network_image/cached_network_image.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:flutter_animate/flutter_animate.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 { | class UniversalImage extends StatelessWidget { | ||||||
|   final String url; |   final String url; | ||||||
|   final double? width, height; |   final double? width, height; | ||||||
| @@ -14,6 +17,7 @@ class UniversalImage extends StatelessWidget { | |||||||
|   final bool noProgressIndicator; |   final bool noProgressIndicator; | ||||||
|   final bool noErrorWidget; |   final bool noErrorWidget; | ||||||
|   final double? cacheWidth, cacheHeight; |   final double? cacheWidth, cacheHeight; | ||||||
|  |   final FilterQuality? filterQuality; | ||||||
|  |  | ||||||
|   const UniversalImage( |   const UniversalImage( | ||||||
|     this.url, { |     this.url, { | ||||||
| @@ -25,45 +29,43 @@ class UniversalImage extends StatelessWidget { | |||||||
|     this.noErrorWidget = false, |     this.noErrorWidget = false, | ||||||
|     this.cacheWidth, |     this.cacheWidth, | ||||||
|     this.cacheHeight, |     this.cacheHeight, | ||||||
|  |     this.filterQuality, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; |     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||||
|     final double? resizeHeight = |     final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null; | ||||||
|         cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null; |     final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null; | ||||||
|     final double? resizeWidth = |  | ||||||
|         cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null; |  | ||||||
|  |  | ||||||
|     return Image( |     return Image( | ||||||
|       image: ResizeImage( |       filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality, | ||||||
|         UniversalImage.provider(url), |       image: kIsWeb | ||||||
|         width: resizeWidth?.round(), |           ? UniversalImage.provider(url) | ||||||
|         height: resizeHeight?.round(), |           : ResizeImage( | ||||||
|         policy: ResizeImagePolicy.fit, |               UniversalImage.provider(url), | ||||||
|       ), |               width: resizeWidth?.round(), | ||||||
|  |               height: resizeHeight?.round(), | ||||||
|  |               policy: ResizeImagePolicy.fit, | ||||||
|  |             ), | ||||||
|       width: width, |       width: width, | ||||||
|       height: height, |       height: height, | ||||||
|       fit: fit, |       fit: fit, | ||||||
|       loadingBuilder: noProgressIndicator |       loadingBuilder: noProgressIndicator | ||||||
|           ? null |           ? null | ||||||
|           : (BuildContext context, Widget child, |           : (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { | ||||||
|               ImageChunkEvent? loadingProgress) { |  | ||||||
|               if (loadingProgress == null) return child; |               if (loadingProgress == null) return child; | ||||||
|               return Center( |               return Center( | ||||||
|                 child: TweenAnimationBuilder( |                 child: TweenAnimationBuilder( | ||||||
|                   tween: Tween( |                   tween: Tween( | ||||||
|                     begin: 0, |                     begin: 0, | ||||||
|                     end: loadingProgress.expectedTotalBytes != null |                     end: loadingProgress.expectedTotalBytes != null | ||||||
|                         ? loadingProgress.cumulativeBytesLoaded / |                         ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! | ||||||
|                             loadingProgress.expectedTotalBytes! |  | ||||||
|                         : 0, |                         : 0, | ||||||
|                   ), |                   ), | ||||||
|                   duration: const Duration(milliseconds: 300), |                   duration: const Duration(milliseconds: 300), | ||||||
|                   builder: (context, value, _) => CircularProgressIndicator( |                   builder: (context, value, _) => CircularProgressIndicator( | ||||||
|                     value: loadingProgress.expectedTotalBytes != null |                     value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, | ||||||
|                         ? value.toDouble() |  | ||||||
|                         : null, |  | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ); |               ); | ||||||
| @@ -94,10 +96,13 @@ class UniversalImage extends StatelessWidget { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   static ImageProvider provider(String url) { |   static ImageProvider provider(String url) { | ||||||
|     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { |     // This place used to use network image or cached network image depending on the platform. | ||||||
|       return CachedNetworkImageProvider(url); |     // But now the cached network image is working on every platform. | ||||||
|     } |     // So we just use it now. | ||||||
|     return NetworkImage(url); |     return CachedNetworkImageProvider( | ||||||
|  |       url, | ||||||
|  |       imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import firebase_messaging | |||||||
| import flutter_udid | import flutter_udid | ||||||
| import flutter_webrtc | import flutter_webrtc | ||||||
| import gal | import gal | ||||||
|  | import in_app_review | ||||||
| import livekit_client | import livekit_client | ||||||
| import media_kit_libs_macos_video | import media_kit_libs_macos_video | ||||||
| import media_kit_video | import media_kit_video | ||||||
| @@ -41,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | |||||||
|   FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) |   FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) | ||||||
|   FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) |   FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) | ||||||
|   GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) |   GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) | ||||||
|  |   InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) | ||||||
|   LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) |   LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) | ||||||
|   MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) |   MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) | ||||||
|   MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) |   MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ PODS: | |||||||
|     - Firebase/Analytics (= 11.4.0) |     - Firebase/Analytics (= 11.4.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - firebase_core (3.8.1): |   - firebase_core (3.9.0): | ||||||
|     - Firebase/CoreOnly (~> 11.4.0) |     - Firebase/CoreOnly (~> 11.4.0) | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - firebase_messaging (15.1.6): |   - firebase_messaging (15.1.6): | ||||||
| @@ -132,6 +132,8 @@ PODS: | |||||||
|   - GoogleUtilities/UserDefaults (8.0.2): |   - GoogleUtilities/UserDefaults (8.0.2): | ||||||
|     - GoogleUtilities/Logger |     - GoogleUtilities/Logger | ||||||
|     - GoogleUtilities/Privacy |     - GoogleUtilities/Privacy | ||||||
|  |   - in_app_review (2.0.0): | ||||||
|  |     - FlutterMacOS | ||||||
|   - livekit_client (2.3.2): |   - livekit_client (2.3.2): | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
| @@ -186,6 +188,7 @@ DEPENDENCIES: | |||||||
|   - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) |   - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) | ||||||
|   - FlutterMacOS (from `Flutter/ephemeral`) |   - FlutterMacOS (from `Flutter/ephemeral`) | ||||||
|   - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) |   - 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`) |   - 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_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`) |   - 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 |     :path: Flutter/ephemeral | ||||||
|   gal: |   gal: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin |     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin | ||||||
|  |   in_app_review: | ||||||
|  |     :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos | ||||||
|   livekit_client: |   livekit_client: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos |     :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos | ||||||
|   media_kit_libs_macos_video: |   media_kit_libs_macos_video: | ||||||
| @@ -279,20 +284,21 @@ SPEC CHECKSUMS: | |||||||
|   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d |   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d | ||||||
|   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 |   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 | ||||||
|   firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef |   firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef | ||||||
|   firebase_core: e4a35c426636a2cce00a5163df7ba69bfd0cca57 |   firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f | ||||||
|   firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf |   firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf | ||||||
|   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 |   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 | ||||||
|   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 |   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 | ||||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 |   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||||
|   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 |   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 | ||||||
|   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 |   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 | ||||||
|   flutter_udid: 6b2b89780c3dfeecf0047bdf93f622d6416b1c07 |   flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 | ||||||
|   flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a |   flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a | ||||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 |   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||||
|   gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 |   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||||
|   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e |   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e | ||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d |   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||||
|  |   in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 | ||||||
|   livekit_client: 9fdcb22df3de55e6d4b24bdc3b5eb1c0269d774a |   livekit_client: 9fdcb22df3de55e6d4b24bdc3b5eb1c0269d774a | ||||||
|   media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 |   media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 | ||||||
|   media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 |   media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 | ||||||
|   | |||||||
| @@ -31,16 +31,18 @@ | |||||||
| 	<key>NSPrincipalClass</key> | 	<key>NSPrincipalClass</key> | ||||||
| 	<string>NSApplication</string> | 	<string>NSApplication</string> | ||||||
| 	<key>NSCameraUsageDescription</key> | 	<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> | 	<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> | 	<key>NSPhotoLibraryUsageDescription</key> | ||||||
| 	<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string> | 	<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string> | ||||||
| 	<key>CFBundleDisplayName</key> | 	<key>CFBundleDisplayName</key> | ||||||
| 	<string>$(PRODUCT_NAME)</string> | 	<string>$(PRODUCT_NAME)</string> | ||||||
|  | 	<key>NSCameraUseContinuityCameraDeviceType</key> | ||||||
|  | 	<string></string> | ||||||
| 	<key>ITSAppUsesNonExemptEncryption</key> | 	<key>ITSAppUsesNonExemptEncryption</key> | ||||||
| 	<false/> | 	<false/> | ||||||
| 	<key>NSPhotoLibraryAddUsageDescription</key> |  | ||||||
| 	<string>Grant access to Photo Library will allow Solian download photo to album for you.</string> |  | ||||||
| </dict> | </dict> | ||||||
| </plist> | </plist> | ||||||
|   | |||||||
							
								
								
									
										178
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						| @@ -13,10 +13,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: _flutterfire_internals |       name: _flutterfire_internals | ||||||
|       sha256: eae3133cbb06de9205899b822e3897fc6a8bc278ad4c944b4ce612689369694b |       sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.47" |     version: "1.3.48" | ||||||
|   _macros: |   _macros: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: dart |     description: dart | ||||||
| @@ -50,10 +50,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: archive |       name: archive | ||||||
|       sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8" |       sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.0.1" |     version: "4.0.2" | ||||||
|   args: |   args: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -122,50 +122,50 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build |       name: build | ||||||
|       sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" |       sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.1" |     version: "2.4.2" | ||||||
|   build_config: |   build_config: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build_config |       name: build_config | ||||||
|       sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 |       sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.1" |     version: "1.1.2" | ||||||
|   build_daemon: |   build_daemon: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build_daemon |       name: build_daemon | ||||||
|       sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" |       sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.0.2" |     version: "4.0.3" | ||||||
|   build_resolvers: |   build_resolvers: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build_resolvers |       name: build_resolvers | ||||||
|       sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" |       sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.2" |     version: "2.4.3" | ||||||
|   build_runner: |   build_runner: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: |     description: | ||||||
|       name: build_runner |       name: build_runner | ||||||
|       sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" |       sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.13" |     version: "2.4.14" | ||||||
|   build_runner_core: |   build_runner_core: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build_runner_core |       name: build_runner_core | ||||||
|       sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 |       sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "7.3.2" |     version: "8.0.0" | ||||||
|   built_collection: |   built_collection: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -178,10 +178,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: built_value |       name: built_value | ||||||
|       sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb |       sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "8.9.2" |     version: "8.9.3" | ||||||
|   cached_network_image: |   cached_network_image: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -343,13 +343,13 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.7" |     version: "2.3.7" | ||||||
|   dart_webrtc: |   dart_webrtc: | ||||||
|     dependency: transitive |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: dart_webrtc |       name: dart_webrtc | ||||||
|       sha256: c664ad88d5646735753add421ee2118486c100febef5e92b7f59cdbabf6a51f6 |       sha256: e65506edb452148220efab53d8d2f8bb9d827bd8bcd53cf3a3e6df70b27f3d86 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.4.9" |     version: "1.4.10" | ||||||
|   dbus: |   dbus: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -490,10 +490,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: file_picker |       name: file_picker | ||||||
|       sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c" |       sha256: c2376a6aae82358a9f9ccdd7d1f4006d08faa39a2767cce01031d9f593a8bd3b | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "8.1.4" |     version: "8.1.6" | ||||||
|   file_saver: |   file_saver: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -562,26 +562,26 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: firebase_core |       name: firebase_core | ||||||
|       sha256: fef81a53ba1ca618def1f8bef4361df07968434e62cb204c1fb90bb880a03da2 |       sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.8.1" |     version: "3.9.0" | ||||||
|   firebase_core_platform_interface: |   firebase_core_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_core_platform_interface |       name: firebase_core_platform_interface | ||||||
|       sha256: b94b217e3ad745e784960603d33d99471621ecca151c99c670869b76e50ad2a6 |       sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.3.1" |     version: "5.4.0" | ||||||
|   firebase_core_web: |   firebase_core_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_core_web |       name: firebase_core_web | ||||||
|       sha256: "9e69806bb3d905aeec3c1242e0e1475de6ea6d48f456af29d598fb229a2b4e5e" |       sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.18.2" |     version: "2.19.0" | ||||||
|   firebase_messaging: |   firebase_messaging: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -614,6 +614,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.1" |     version: "1.1.1" | ||||||
|  |   fl_chart: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: fl_chart | ||||||
|  |       sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.70.0" | ||||||
|   flutter: |   flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -627,6 +635,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.5.2" |     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: |   flutter_cache_manager: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -635,6 +651,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.4.1" |     version: "3.4.1" | ||||||
|  |   flutter_colorpicker: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: flutter_colorpicker | ||||||
|  |       sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "1.1.0" | ||||||
|   flutter_context_menu: |   flutter_context_menu: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -692,10 +716,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: flutter_plugin_android_lifecycle |       name: flutter_plugin_android_lifecycle | ||||||
|       sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" |       sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.23" |     version: "2.0.24" | ||||||
|   flutter_shaders: |   flutter_shaders: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -729,10 +753,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_udid |       name: flutter_udid | ||||||
|       sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" |       sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.0" |     version: "3.0.1" | ||||||
|   flutter_web_plugins: |   flutter_web_plugins: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -742,10 +766,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: flutter_webrtc |       name: flutter_webrtc | ||||||
|       sha256: "4838217405c42cce422698eacc9c2e17089b9c05322be899c0a725107dcddbdc" |       sha256: "430859fb5b763d7556d06ef287cfca582e17d9a2dc36da26017f25a5c0b2523e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.12.3" |     version: "0.12.4" | ||||||
|   freezed: |   freezed: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: |     description: | ||||||
| @@ -774,10 +798,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: gal |       name: gal | ||||||
|       sha256: "54c9b72528efce7c66234f3b6dd01cb0304fd8af8196de15571d7bdddb940977" |       sha256: "2771519c8b29f784d5e27f4efc2667667eef51c6c47cccaa0435a8fe8aa208e4" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.1" | ||||||
|   gap: |   gap: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -870,10 +894,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: http_multi_server |       name: http_multi_server | ||||||
|       sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" |       sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.2.1" |     version: "3.2.2" | ||||||
|   http_parser: |   http_parser: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -894,10 +918,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: image |       name: image | ||||||
|       sha256: "599d08e369969bdf83138f5b4e0a7e823d3f992f23b8a64dd626877c37013533" |       sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.4.0" |     version: "4.5.2" | ||||||
|   image_picker: |   image_picker: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -962,6 +986,22 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.2.1+1" |     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: |   intl: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1038,10 +1078,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: lints |       name: lints | ||||||
|       sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" |       sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.1.0" |     version: "5.1.1" | ||||||
|   livekit_client: |   livekit_client: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1454,10 +1494,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: pubspec_parse |       name: pubspec_parse | ||||||
|       sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 |       sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.0" |     version: "1.4.0" | ||||||
|   qr: |   qr: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1598,10 +1638,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences |       name: shared_preferences | ||||||
|       sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" |       sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.3" |     version: "2.3.4" | ||||||
|   shared_preferences_android: |   shared_preferences_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1691,10 +1731,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: source_helper |       name: source_helper | ||||||
|       sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" |       sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.4" |     version: "1.3.5" | ||||||
|   source_span: |   source_span: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1771,10 +1811,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: stream_transform |       name: stream_transform | ||||||
|       sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" |       sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     version: "2.1.1" | ||||||
|   string_scanner: |   string_scanner: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1975,6 +2015,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.4" |     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: |   very_good_infinite_list: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -2003,26 +2051,26 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: wakelock_plus |       name: wakelock_plus | ||||||
|       sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 |       sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.2.8" |     version: "1.2.10" | ||||||
|   wakelock_plus_platform_interface: |   wakelock_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: wakelock_plus_platform_interface |       name: wakelock_plus_platform_interface | ||||||
|       sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" |       sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.2.1" |     version: "1.2.2" | ||||||
|   watcher: |   watcher: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: watcher |       name: watcher | ||||||
|       sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" |       sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.0" |     version: "1.1.1" | ||||||
|   web: |   web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2071,6 +2119,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.5" |     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: |   xdg_directories: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2091,10 +2147,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: yaml |       name: yaml | ||||||
|       sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" |       sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.1.2" |     version: "3.1.3" | ||||||
| sdks: | sdks: | ||||||
|   dart: ">=3.6.0-0 <4.0.0" |   dart: ">=3.6.0 <4.0.0" | ||||||
|   flutter: ">=3.24.0" |   flutter: ">=3.27.0" | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						| @@ -1,5 +1,5 @@ | |||||||
| name: surface | name: surface | ||||||
| description: "A new Flutter project." | description: "The application for Solar Network" | ||||||
| # The following line prevents the package from being accidentally published to | # The following line prevents the package from being accidentally published to | ||||||
| # pub.dev using `flutter pub publish`. This is preferred for private packages. | # 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 | 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 | # 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 | # 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. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 2.0.1+30 | version: 2.1.1+39 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.5.4 |   sdk: ^3.5.4 | ||||||
| @@ -61,7 +61,7 @@ dependencies: | |||||||
|   relative_time: ^5.0.0 |   relative_time: ^5.0.0 | ||||||
|   image_picker: ^1.1.2 |   image_picker: ^1.1.2 | ||||||
|   cross_file: ^0.3.4+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 |   croppy: ^1.3.1 | ||||||
|   flutter_expandable_fab: ^2.3.0 |   flutter_expandable_fab: ^2.3.0 | ||||||
|   dropdown_button2: ^2.3.9 |   dropdown_button2: ^2.3.9 | ||||||
| @@ -86,6 +86,7 @@ dependencies: | |||||||
|   media_kit_libs_video: ^1.0.5 |   media_kit_libs_video: ^1.0.5 | ||||||
|   pasteboard: ^0.3.0 |   pasteboard: ^0.3.0 | ||||||
|   synchronized: ^3.3.0+3 |   synchronized: ^3.3.0+3 | ||||||
|  |   dart_webrtc: ^1.4.10 | ||||||
|   livekit_client: ^2.3.1+hotfix.1 |   livekit_client: ^2.3.1+hotfix.1 | ||||||
|   wakelock_plus: ^1.2.8 |   wakelock_plus: ^1.2.8 | ||||||
|   permission_handler: ^11.3.1 |   permission_handler: ^11.3.1 | ||||||
| @@ -106,6 +107,12 @@ dependencies: | |||||||
|   flutter_svg: ^2.0.16 |   flutter_svg: ^2.0.16 | ||||||
|   home_widget: ^0.7.0 |   home_widget: ^0.7.0 | ||||||
|   receive_sharing_intent: ^1.8.1 |   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 | ||||||
|  |   flutter_colorpicker: ^1.1.0 | ||||||
|  |   fl_chart: ^0.70.0 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
| @@ -138,6 +145,7 @@ flutter: | |||||||
|   # To add assets to your application, add an assets section, like this: |   # To add assets to your application, add an assets section, like this: | ||||||
|   assets: |   assets: | ||||||
|     - assets/icon/icon.png |     - assets/icon/icon.png | ||||||
|  |     - assets/icon/icon-dark.png | ||||||
|     - assets/icon/icon-light-radius.png |     - assets/icon/icon-light-radius.png | ||||||
|     - assets/translations/ |     - assets/translations/ | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||