Compare commits
	
		
			213 Commits
		
	
	
		
			2eb1f4b52b
			...
			2.3.2+68
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5ddd4fed2e | |||
| 48b6d5f6c1 | |||
| b83b0b5efb | |||
| cb24bd953d | |||
| 4937dee182 | |||
| d612097bb1 | |||
| 058d668b6b | |||
| 8b19462c3a | |||
| 0a381ef09b | |||
| 9b84e912b2 | |||
| b3254e0f2f | |||
| f0a3bbe023 | |||
| df81c84438 | |||
| 8b12395fca | |||
| cb2b71d194 | |||
| 7ed508e2bb | |||
| dad869967e | |||
| 2d5b3b554e | |||
| 74882116e3 | |||
| a97c3bce3a | |||
| 1aa70827dc | |||
| fe028860e9 | |||
| a2d2ce4d38 | |||
| 167c11b9eb | |||
| 8cb3933fcc | |||
| 3818328afe | |||
| 11627e2455 | |||
| 3f82c06ff8 | |||
| 2350f59131 | |||
| 9fe7c9530a | |||
| 52f1826e91 | |||
| 28a4c86dbf | |||
| 85e48ce03b | |||
| efef61a8ea | |||
| 10ead95af9 | |||
| 838ee4d55d | |||
| 13e42429a9 | |||
| c6ce3fe2b7 | |||
| ae9a7eb0fd | |||
| 5d6fb2442f | |||
| 5a85985534 | |||
| c80499db03 | |||
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | |||
| db8871a455 | |||
| 38dcaa6066 | |||
| 03275b46ca | |||
| cf3b482fef | |||
| aa4c04d4ef | |||
| 73b82f65e4 | |||
| 9471fe40fe | |||
| 0d1e18735e | |||
| 8bb62b5992 | |||
| 1e8a6dea5b | |||
| 5c2804cc4d | |||
| 0dbb8f132a | |||
| 3395f3dbd0 | |||
| d258ba776e | |||
| 0dcfcaad56 | |||
| 687e720956 | |||
| 180876949e | |||
| 9718965809 | |||
| 5377161fb0 | |||
| 963e538ae5 | |||
| a355e3bf90 | |||
| cb4a2598c8 | |||
| 950612dc07 | |||
| cbd1eaf1af | |||
| ac41cbd99f | |||
| 9f9c90abc4 | |||
| 87029e3538 | |||
| 127d9adc09 | |||
| c82dc7ad85 | |||
| 36bcff7a7c | |||
| 38201b547a | |||
| ed0334fcda | |||
| fbb486b90b | |||
| 9b34f385d5 | |||
| bb7b731602 | |||
| 19076f8136 | |||
| dc77a936ce | |||
| 7f58710c6f | |||
| 068ddcdcdc | |||
| f4e9252ca0 | |||
| 3b1e918117 | |||
| ed7981fdaf | |||
| 9698ca53e4 | |||
| ddc1dc7daf | |||
| 1625a957f8 | |||
| 2dc50d627e | |||
| 2ffde9a3dd | |||
| 5967a91ae1 | |||
| 32c1effcb5 | |||
| 9d0e19c56f | |||
| acf4e634fe | |||
| 25942c2338 | |||
| a4f81f6ba1 | |||
| c1b9090e51 | |||
| f494f70003 | |||
| fb2a55a909 | |||
| 4edfa7fd50 | |||
| d699cac9b1 | |||
| c0428e12c1 | |||
| 55f434ff05 | |||
| f2b3bdda2d | |||
| 1f6bf33b0e | |||
| e2027b1a32 | |||
| 2b3a58b55e | |||
| 6ac536412a | |||
| 52f8ffe4e4 | |||
| aca81431aa | |||
| 1fadd850b7 | |||
| ed2a9a21b6 | |||
| 57279eb3e4 | |||
| c403a2914a | |||
| bcb176344c | |||
| ecf362cffc | |||
| f4ab7671d8 | |||
| a2a3018917 | |||
| 0bdb664000 | |||
| 9c3b61ce57 | |||
| d06df3d278 | |||
| 547ba19e61 | |||
| cb05ff2e9e | |||
| f614da7918 | |||
| a3c8dafff9 | |||
| fa978a7cd1 | |||
| aaa0a562b4 | |||
| 590a4ce2a6 | |||
| f26edce071 | |||
| 603799ea32 | |||
| a32baf7798 | |||
| 498c9af663 | |||
| 202dbff6d3 | |||
| 96fd64d85d | |||
| e236b7f98b | |||
| 5c7929e618 | |||
| 7ba5260246 | |||
| a6d4947a23 | |||
| 7fbd4e9647 | |||
| 95d926b29f | |||
| f6cf6d0440 | |||
| e503c3f02f | |||
| d4fbdd397e | |||
| 03943a7138 | |||
| 44f2c5fe0e | |||
| bb66d5b684 | |||
| 1fca36293d | |||
| 2c7dc8c2ea | |||
| cf0df91d8c | |||
| 91c85e8a58 | |||
| 2851780dda | |||
| 00fd58fb97 | |||
| ee7d0ddd25 | |||
| 7656c08832 | |||
| 619c90cdd9 | |||
| 168d51c9fe | |||
| d4b831f98e | |||
| 4d96a15c31 | |||
| 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 | 
@@ -1,12 +1,12 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "sync": {
 | 
					  "sync": {
 | 
				
			||||||
    "region": "solian-next",
 | 
					    "region": "solian",
 | 
				
			||||||
    "configPath": "roadsign.toml"
 | 
					    "configPath": "roadsign.toml"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "deployments": [
 | 
					  "deployments": [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "region": "solian-next",
 | 
					      "region": "solian",
 | 
				
			||||||
      "site": "solian-next-web",
 | 
					      "site": "solian-web",
 | 
				
			||||||
      "path": "build/web"
 | 
					      "path": "build/web"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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.
 | 
					 | 
				
			||||||
@@ -15,6 +15,7 @@ analyzer:
 | 
				
			|||||||
    - "**/*.freezed.dart"
 | 
					    - "**/*.freezed.dart"
 | 
				
			||||||
  errors:
 | 
					  errors:
 | 
				
			||||||
    invalid_annotation_target: ignore # Due to freezed + json_serializable issue, ref https://github.com/rrousselGit/freezed/issues/488#issuecomment-894358980
 | 
					    invalid_annotation_target: ignore # Due to freezed + json_serializable issue, ref https://github.com/rrousselGit/freezed/issues/488#issuecomment-894358980
 | 
				
			||||||
 | 
					    deprecated_member_use: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
linter:
 | 
					linter:
 | 
				
			||||||
  # The lint rules applied to this project can be customized in the
 | 
					  # The lint rules applied to this project can be customized in the
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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'
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,33 +17,27 @@
 | 
				
			|||||||
        android:label="Solian"
 | 
					        android:label="Solian"
 | 
				
			||||||
        android:name="${applicationName}"
 | 
					        android:name="${applicationName}"
 | 
				
			||||||
        android:icon="@mipmap/ic_launcher"
 | 
					        android:icon="@mipmap/ic_launcher"
 | 
				
			||||||
 | 
					        android:enableOnBackInvokedCallback="true"
 | 
				
			||||||
        android:requestLegacyExternalStorage="true">
 | 
					        android:requestLegacyExternalStorage="true">
 | 
				
			||||||
 | 
					        <meta-data
 | 
				
			||||||
 | 
					            android:name="flutterEmbedding"
 | 
				
			||||||
 | 
					            android:value="2" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
            android:name=".MainActivity"
 | 
					            android:name=".MainActivity"
 | 
				
			||||||
            android:exported="true"
 | 
					            android:exported="true"
 | 
				
			||||||
            android:launchMode="singleTask"
 | 
					            android:launchMode="singleInstance"
 | 
				
			||||||
            android:taskAffinity=""
 | 
					            android:taskAffinity=""
 | 
				
			||||||
            android:theme="@style/LaunchTheme"
 | 
					            android:theme="@style/LaunchTheme"
 | 
				
			||||||
            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 +94,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
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								api/Paperclip/Activate Boost.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Activate Boost
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/uc/boosts/1/activate
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "client_id": "{{third_client_id}}",
 | 
				
			||||||
 | 
					    "client_secret":"{{third_client_tk}}",
 | 
				
			||||||
 | 
					    "type": "general",
 | 
				
			||||||
 | 
					    "subject": "Merry Christmas!",
 | 
				
			||||||
 | 
					    "subtitle": "一条来自 Solar Network 团队的信息",
 | 
				
			||||||
 | 
					    "content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
 | 
				
			||||||
 | 
					    "metadata": {
 | 
				
			||||||
 | 
					      "image": "6EqsYQwmFRCkbmhR"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "priority": 10
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								api/Paperclip/Stickers/Create Sticker Pack.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Create Sticker Pack
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/uc/stickers/packs
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "prefix": "cat",
 | 
				
			||||||
 | 
					    "name": "Solar Network full of Cats!",
 | 
				
			||||||
 | 
					    "description": "The sticker packs is full of stickers which related with cats!"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								api/Paperclip/Stickers/Create Sticker.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Create Sticker
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/uc/stickers
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "alias": "BaLoading",
 | 
				
			||||||
 | 
					    "name": "BaLoading",
 | 
				
			||||||
 | 
					    "attachment_id": "2JCI2uh21mKkfk9P",
 | 
				
			||||||
 | 
					    "pack_id": 3
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								api/Paperclip/Stickers/Get Sticker Packs.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Get Sticker Packs
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 3
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/uc/stickers/packs
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								api/Paperclip/Stickers/Get Stickers.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Get Stickers
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 4
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/uc/stickers?take=10
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					params:query {
 | 
				
			||||||
 | 
					  take: 10
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								api/Passport/Developer Notify All Users.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Developer Notify All Users
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/id/dev/notify/all
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "client_id": "{{third_client_id}}",
 | 
				
			||||||
 | 
					    "client_secret":"{{third_client_tk}}",
 | 
				
			||||||
 | 
					    "type": "general",
 | 
				
			||||||
 | 
					    "subject": "新年快乐!",
 | 
				
			||||||
 | 
					    "subtitle": "一条来自 Solar Network 团队的信息",
 | 
				
			||||||
 | 
					    "content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
 | 
				
			||||||
 | 
					    "metadata": {
 | 
				
			||||||
 | 
					      "image": "D2EDbcrsTugs3xk5"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "priority": 10
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								api/Passport/Developer Notify One User.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Developer Notify One User
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/id/dev/notify/122
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "client_id": "{{third_client_id}}",
 | 
				
			||||||
 | 
					    "client_secret":"{{third_client_tk}}",
 | 
				
			||||||
 | 
					    "type": "general",
 | 
				
			||||||
 | 
					    "subject": "处理该帐号 @solian 的决定",
 | 
				
			||||||
 | 
					    "subtitle": "违反用户协议",
 | 
				
			||||||
 | 
					    "content": "您的帐号违反了我们用户协议中关于冒充我们官方的行为,至此做出停权的决定。还请见谅。该决定是最终决定,不接受上诉。",
 | 
				
			||||||
 | 
					    "priority": 10
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								api/Reader/List News Sources.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: List News Sources
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 3
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/re/well-known/sources
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								api/Reader/List News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: List News
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					params:query {
 | 
				
			||||||
 | 
					  take: 10
 | 
				
			||||||
 | 
					  offset: 0
 | 
				
			||||||
 | 
					  source: shadiao
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								api/Reader/Trigger Scan News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Trigger Scan News
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/re/admin/scan
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "sources": ["taiwan-ltn"],
 | 
				
			||||||
 | 
					    "eager": true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								api/Wallet/Create Order.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Create Order
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/wa/orders
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "client_id": "highland-mc",
 | 
				
			||||||
 | 
					    "client_secret": "(3^DLAvo3v",
 | 
				
			||||||
 | 
					    "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
 | 
				
			||||||
 | 
					    "amount": 500
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								api/Wallet/Create Transaction.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Create Transaction
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 3
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/wa/transactions
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "client_id": "alphabot",
 | 
				
			||||||
 | 
					    "client_secret": "_uR0sVnHTh",
 | 
				
			||||||
 | 
					    "remark": "新年红包",
 | 
				
			||||||
 | 
					    "amount": 9705,
 | 
				
			||||||
 | 
					    "payee_id": 2
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								api/Wallet/Get Order.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Get Order
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/wa/orders/4
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "client_id": "highland-mc",
 | 
				
			||||||
 | 
					    "client_secret": "(3^DLAvo3v",
 | 
				
			||||||
 | 
					    "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
 | 
				
			||||||
 | 
					    "amount": 500
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								api/Wallet/Get Transaction.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Get Transaction
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 4
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/wa/transactions/67
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "client_id": "highland-mc",
 | 
				
			||||||
 | 
					    "client_secret": "(3^DLAvo3v",
 | 
				
			||||||
 | 
					    "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
 | 
				
			||||||
 | 
					    "amount": 500
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								api/WatchTower/Run Database Maintenance.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Run Database Maintenance
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/wt/maintenance/database
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								api/bruno.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "version": "1",
 | 
				
			||||||
 | 
					  "name": "Solar Network",
 | 
				
			||||||
 | 
					  "type": "collection",
 | 
				
			||||||
 | 
					  "ignore": [
 | 
				
			||||||
 | 
					    "node_modules",
 | 
				
			||||||
 | 
					    ".git"
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								api/collection.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					auth {
 | 
				
			||||||
 | 
					  mode: bearer
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					auth:bearer {
 | 
				
			||||||
 | 
					  token: {{atk}}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								api/environments/Prod.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					vars {
 | 
				
			||||||
 | 
					  endpoint: https://api.sn.solsynth.dev
 | 
				
			||||||
 | 
					  third_client_id: alphabot
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					vars:secret [
 | 
				
			||||||
 | 
					  atk,
 | 
				
			||||||
 | 
					  third_client_tk
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								assets/icon/tray-icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 16 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon/tray-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 228 KiB  | 
@@ -17,12 +17,17 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "Edit Profile",
 | 
					  "screenAccountProfileEdit": "Edit Profile",
 | 
				
			||||||
  "screenAbuseReport": "Abuse Reports",
 | 
					  "screenAbuseReport": "Abuse Reports",
 | 
				
			||||||
  "screenSettings": "Settings",
 | 
					  "screenSettings": "Settings",
 | 
				
			||||||
 | 
					  "screenAccountSettings": "Account Settings",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "Auth Factors",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "Wallet",
 | 
				
			||||||
 | 
					  "screenNews": "News",
 | 
				
			||||||
  "screenAlbum": "Album",
 | 
					  "screenAlbum": "Album",
 | 
				
			||||||
  "screenChat": "Chat",
 | 
					  "screenChat": "Chat",
 | 
				
			||||||
  "screenChatManage": "Edit Channel",
 | 
					  "screenChatManage": "Edit Channel",
 | 
				
			||||||
  "screenChatNew": "New Channel",
 | 
					  "screenChatNew": "New Channel",
 | 
				
			||||||
  "screenRealm": "Realm",
 | 
					  "screenRealm": "Realm",
 | 
				
			||||||
  "screenRealmManage": "Edit Realm",
 | 
					  "screenRealmManage": "Edit Realm",
 | 
				
			||||||
 | 
					  "screenRealmDiscovery": "Realm Discovery",
 | 
				
			||||||
  "screenRealmNew": "New Realm",
 | 
					  "screenRealmNew": "New Realm",
 | 
				
			||||||
  "screenNotification": "Notification",
 | 
					  "screenNotification": "Notification",
 | 
				
			||||||
  "screenPostSearch": "Search Posts",
 | 
					  "screenPostSearch": "Search Posts",
 | 
				
			||||||
@@ -57,7 +62,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 {}",
 | 
				
			||||||
@@ -103,8 +108,18 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "Enter the code",
 | 
					  "loginEnterPassword": "Enter the code",
 | 
				
			||||||
  "loginSuccess": "Logged in as {}",
 | 
					  "loginSuccess": "Logged in as {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "Delete Auth Factor",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
 | 
				
			||||||
  "authFactorPassword": "Password",
 | 
					  "authFactorPassword": "Password",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "The password you set when you registered.",
 | 
				
			||||||
  "authFactorEmail": "Email verification code",
 | 
					  "authFactorEmail": "Email verification code",
 | 
				
			||||||
 | 
					  "authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
 | 
				
			||||||
 | 
					  "authFactorTOTP": "Time-based OTP",
 | 
				
			||||||
 | 
					  "authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
 | 
				
			||||||
 | 
					  "authFactorInAppNotify": "In-app notification",
 | 
				
			||||||
 | 
					  "authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
 | 
				
			||||||
 | 
					  "authFactorAdd": "Add a factor",
 | 
				
			||||||
 | 
					  "authFactorAddSubtitle": "Provide another way to login your account.",
 | 
				
			||||||
  "accountIntroTitle": "Hello there!",
 | 
					  "accountIntroTitle": "Hello there!",
 | 
				
			||||||
  "accountIntroSubtitle": "Pick an option below to get started.",
 | 
					  "accountIntroSubtitle": "Pick an option below to get started.",
 | 
				
			||||||
  "accountLogout": "Logout",
 | 
					  "accountLogout": "Logout",
 | 
				
			||||||
@@ -113,8 +128,14 @@
 | 
				
			|||||||
  "accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
 | 
					  "accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
 | 
				
			||||||
  "accountPublishers": "Your publishers",
 | 
					  "accountPublishers": "Your publishers",
 | 
				
			||||||
  "accountPublishersSubtitle": "Manage your publish identities.",
 | 
					  "accountPublishersSubtitle": "Manage your publish identities.",
 | 
				
			||||||
 | 
					  "accountSettings": "Account Settings",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "Manage your account and make it yours.",
 | 
				
			||||||
  "accountProfileEdit": "Edit your profile",
 | 
					  "accountProfileEdit": "Edit your profile",
 | 
				
			||||||
  "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
 | 
					  "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
 | 
				
			||||||
 | 
					  "accountWallet": "Wallet",
 | 
				
			||||||
 | 
					  "accountWalletSubtitle": "View your balance and transactions.",
 | 
				
			||||||
 | 
					  "factorSettings": "Auth Factors",
 | 
				
			||||||
 | 
					  "factorSettingsSubtitle": "Manage your authentication factors.",
 | 
				
			||||||
  "accountProfileEditApplied": "Profile modification applied.",
 | 
					  "accountProfileEditApplied": "Profile modification applied.",
 | 
				
			||||||
  "publishersNew": "New Publisher",
 | 
					  "publishersNew": "New Publisher",
 | 
				
			||||||
  "publisherNewSubtitle": "Create a new publisher identity.",
 | 
					  "publisherNewSubtitle": "Create a new publisher identity.",
 | 
				
			||||||
@@ -134,18 +155,24 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
 | 
					  "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
 | 
				
			||||||
  "writePostTypeStory": "Post a story",
 | 
					  "writePostTypeStory": "Post a story",
 | 
				
			||||||
  "writePostTypeArticle": "Write an article",
 | 
					  "writePostTypeArticle": "Write an article",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "Ask a question",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "Post a video",
 | 
				
			||||||
  "fieldPostPublisher": "Post publisher",
 | 
					  "fieldPostPublisher": "Post publisher",
 | 
				
			||||||
  "fieldPostContent": "What happened?!",
 | 
					  "fieldPostContent": "What happened?!",
 | 
				
			||||||
  "fieldPostTitle": "Title",
 | 
					  "fieldPostTitle": "Title",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "Answer Rewards (Source Points)",
 | 
				
			||||||
  "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",
 | 
				
			||||||
  "postPublishedUntil": "Published Until",
 | 
					  "postPublishedUntil": "Published Until",
 | 
				
			||||||
  "postEditingNotice": "You're about to editing a post that posted {}.",
 | 
					  "postEditingNotice": "You're about to editing a post that posted by {}.",
 | 
				
			||||||
  "postReplyingNotice": "You're about to reply to a post that posted {}.",
 | 
					  "postReplyingNotice": "You're about to reply to a post that posted by {}.",
 | 
				
			||||||
  "postRepostingNotice": "You're about to repost a post that posted {}.",
 | 
					  "postRepostingNotice": "You're about to repost a post that posted by {}.",
 | 
				
			||||||
  "postReact": "React",
 | 
					  "postReact": "React",
 | 
				
			||||||
  "postReactions": "Reactions of Post",
 | 
					  "postReactions": "Reactions of Post",
 | 
				
			||||||
  "postReactionUpvote": {
 | 
					  "postReactionUpvote": {
 | 
				
			||||||
@@ -176,12 +203,30 @@
 | 
				
			|||||||
    "other": "{} comments"
 | 
					    "other": "{} comments"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "Appearance",
 | 
					  "settingsAppearance": "Appearance",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "Display Language",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "Set the application language.",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "Follow System",
 | 
				
			||||||
 | 
					  "settingsAppBarTransparent": "Transparent App Bar",
 | 
				
			||||||
 | 
					  "settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
 | 
				
			||||||
 | 
					  "settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
 | 
				
			||||||
 | 
					  "settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
 | 
				
			||||||
  "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.",
 | 
				
			||||||
 | 
					  "settingsFeatures": "Features",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHaptic": "Haptic when Notified",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "Expand Post Link",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "Expand the post link in the post list.",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "Expand Chat Link",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
 | 
				
			||||||
  "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,15 +235,25 @@
 | 
				
			|||||||
  "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.",
 | 
				
			||||||
 | 
					  "settingsAccountLanguage": "Account Language",
 | 
				
			||||||
 | 
					  "settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
 | 
				
			||||||
  "sensitiveContent": "Sensitive Content",
 | 
					  "sensitiveContent": "Sensitive Content",
 | 
				
			||||||
  "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
 | 
					  "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
 | 
				
			||||||
  "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
 | 
					  "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
 | 
				
			||||||
  "sensitiveContentReveal": "Reveal",
 | 
					  "sensitiveContentReveal": "Reveal",
 | 
				
			||||||
  "serverConnecting": "Connecting to server...",
 | 
					  "serverConnecting": "Connecting...",
 | 
				
			||||||
  "serverDisconnected": "Lost connection from server",
 | 
					  "serverDisconnected": "Connection Lost",
 | 
				
			||||||
 | 
					  "serverConnected": "Connected",
 | 
				
			||||||
  "fieldChatAlias": "Channel Alias",
 | 
					  "fieldChatAlias": "Channel Alias",
 | 
				
			||||||
  "fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
 | 
					  "fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
 | 
				
			||||||
  "fieldChatName": "Name",
 | 
					  "fieldChatName": "Name",
 | 
				
			||||||
@@ -265,16 +320,50 @@
 | 
				
			|||||||
    "one": "{} attachment",
 | 
					    "one": "{} attachment",
 | 
				
			||||||
    "other": "{} attachments"
 | 
					    "other": "{} attachments"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "messageTyping": {
 | 
				
			||||||
 | 
					    "one": "{} is typing...",
 | 
				
			||||||
 | 
					    "other": "{} are typing..."
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "fieldAttachmentRandomId": "Random ID",
 | 
				
			||||||
 | 
					  "fieldAttachmentAlt": "Alternative text",
 | 
				
			||||||
  "addAttachmentFromAlbum": "Add from album",
 | 
					  "addAttachmentFromAlbum": "Add from album",
 | 
				
			||||||
  "addAttachmentFromClipboard": "Paste file",
 | 
					  "addAttachmentFromClipboard": "Paste file",
 | 
				
			||||||
  "addAttachmentFromCameraPhoto": "Take photo",
 | 
					  "addAttachmentFromCameraPhoto": "Take photo",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "Take video",
 | 
					  "addAttachmentFromCameraVideo": "Take video",
 | 
				
			||||||
 | 
					  "addAttachmentFromRandomId": "Link via RID",
 | 
				
			||||||
 | 
					  "attachmentDetailInfo": "Attachment details",
 | 
				
			||||||
  "attachmentPastedImage": "Pasted Image",
 | 
					  "attachmentPastedImage": "Pasted Image",
 | 
				
			||||||
  "attachmentInsertLink": "Insert Link",
 | 
					  "attachmentInsertLink": "Insert Link",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "Set as post thumbnail",
 | 
					  "attachmentSetAsPostThumbnail": "Set as post thumbnail",
 | 
				
			||||||
  "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
 | 
					  "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
 | 
				
			||||||
 | 
					  "attachmentCompressVideo": "Re-encode video",
 | 
				
			||||||
  "attachmentSetThumbnail": "Set thumbnail",
 | 
					  "attachmentSetThumbnail": "Set thumbnail",
 | 
				
			||||||
 | 
					  "attachmentSetAlt": "Set alternative text",
 | 
				
			||||||
 | 
					  "attachmentCopyRandomId": "Copy RID",
 | 
				
			||||||
  "attachmentUpload": "Upload",
 | 
					  "attachmentUpload": "Upload",
 | 
				
			||||||
 | 
					  "attachmentInputDialog": "Upload attachments",
 | 
				
			||||||
 | 
					  "attachmentInputUseRandomId": "Use Random ID",
 | 
				
			||||||
 | 
					  "attachmentInputNew": "New Upload",
 | 
				
			||||||
 | 
					  "waitingForUpload": "Waiting for upload",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHint": "Compress a copy of this video",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHintDescription": "Do you want to upload a compress copy of video {}? It will help your audience to preview this video faster and they still can watch the original video. It will take some while to process the video on your device, so please be patient.",
 | 
				
			||||||
 | 
					  "attachmentCompressQuality": "Compress quality",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHighest": "Highest",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityDefault": "Default",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityMedium": "Medium",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityLow": "Low",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality.",
 | 
				
			||||||
 | 
					  "attachmentUploaded": "Uploaded",
 | 
				
			||||||
 | 
					  "attachmentPending": "Pending",
 | 
				
			||||||
 | 
					  "attachmentCopyCompressed": "Copy compressed",
 | 
				
			||||||
 | 
					  "attachmentGotBoosted": "Boosted",
 | 
				
			||||||
 | 
					  "attachmentBoost": "Boost",
 | 
				
			||||||
 | 
					  "attachmentCreateBoost": "Create Boost",
 | 
				
			||||||
 | 
					  "attachmentBoostHint": "Boost is a feature that allows you to upload attachments to a server closer to your audience or a faster content network. This feature is currently in beta and is subject to change. It's all free for now, you can feel free to try, you will get notified when the pricing plan changed.",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegion": "Destination Region",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionAPAC": "Asia Pacific",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionNGB": "Ning Bo, China, Zhejiang",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionHKG": "Hong Kong",
 | 
				
			||||||
  "notification": "Notification",
 | 
					  "notification": "Notification",
 | 
				
			||||||
  "notificationUnreadCount": {
 | 
					  "notificationUnreadCount": {
 | 
				
			||||||
    "zero": "All notifications read",
 | 
					    "zero": "All notifications read",
 | 
				
			||||||
@@ -362,7 +451,32 @@
 | 
				
			|||||||
  "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, {}!",
 | 
				
			||||||
 | 
					  "celebrateLunarNewYear": "Happy lunar new year, {}!",
 | 
				
			||||||
 | 
					  "celebrateMidAutumn": "Happy mid-autumn festival, {}!",
 | 
				
			||||||
 | 
					  "celebrateDragonBoat": "Happy dragon boat festival, {}!",
 | 
				
			||||||
 | 
					  "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 {}",
 | 
				
			||||||
 | 
					  "pendingLunarNewYear": "Lunar new year in {}",
 | 
				
			||||||
 | 
					  "pendingMidAutumn": "Mid-autumn festival in {}",
 | 
				
			||||||
 | 
					  "pendingDragonBoat": "Dragon boat festival 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": {
 | 
				
			||||||
@@ -396,6 +510,7 @@
 | 
				
			|||||||
  "accountJoinedAt": "Joined at {}",
 | 
					  "accountJoinedAt": "Joined at {}",
 | 
				
			||||||
  "accountBirthday": "Born on {}",
 | 
					  "accountBirthday": "Born on {}",
 | 
				
			||||||
  "accountBadge": "Badge",
 | 
					  "accountBadge": "Badge",
 | 
				
			||||||
 | 
					  "accountCheckInNoRecords": "No check-in records",
 | 
				
			||||||
  "badgeCompanyStaff": "Solsynth Staff",
 | 
					  "badgeCompanyStaff": "Solsynth Staff",
 | 
				
			||||||
  "badgeSiteMigration": "Solar Network Native",
 | 
					  "badgeSiteMigration": "Solar Network Native",
 | 
				
			||||||
  "accountStatus": "Status",
 | 
					  "accountStatus": "Status",
 | 
				
			||||||
@@ -404,6 +519,7 @@
 | 
				
			|||||||
  "accountStatusLastSeen": "Last seen at {}",
 | 
					  "accountStatusLastSeen": "Last seen at {}",
 | 
				
			||||||
  "postArticle": "Article on the Solar Network",
 | 
					  "postArticle": "Article on the Solar Network",
 | 
				
			||||||
  "postStory": "Story on the Solar Network",
 | 
					  "postStory": "Story on the Solar Network",
 | 
				
			||||||
 | 
					  "postLocalDraftRestored": "Restored from device",
 | 
				
			||||||
  "articleWrittenAt": "Written at {}",
 | 
					  "articleWrittenAt": "Written at {}",
 | 
				
			||||||
  "articleEditedAt": "Edited at {}",
 | 
					  "articleEditedAt": "Edited at {}",
 | 
				
			||||||
  "attachmentSaved": "Saved to album",
 | 
					  "attachmentSaved": "Saved to album",
 | 
				
			||||||
@@ -439,14 +555,88 @@
 | 
				
			|||||||
  "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",
 | 
				
			||||||
  "postShareImage": "Share via Image",
 | 
					  "postShareImage": "Share via Image",
 | 
				
			||||||
 | 
					  "postGetInsight": "Get Insight",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI Insight",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI may make mistakes, check important information.",
 | 
				
			||||||
  "appInitializing": "Initializing",
 | 
					  "appInitializing": "Initializing",
 | 
				
			||||||
  "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",
 | 
				
			||||||
 | 
					  "shareIntentSendChannel": "Share to Channel",
 | 
				
			||||||
 | 
					  "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.",
 | 
				
			||||||
 | 
					  "postFeaturedComment": "Featured Comment",
 | 
				
			||||||
 | 
					  "postCategoryTechnology": "Technology",
 | 
				
			||||||
 | 
					  "postCategoryGaming": "Gaming",
 | 
				
			||||||
 | 
					  "postCategoryLife": "Life",
 | 
				
			||||||
 | 
					  "postCategoryArts": "Arts",
 | 
				
			||||||
 | 
					  "postCategorySports": "Sports",
 | 
				
			||||||
 | 
					  "postCategoryMusic": "Music",
 | 
				
			||||||
 | 
					  "postCategoryNews": "News",
 | 
				
			||||||
 | 
					  "postCategoryKnowledge": "Knowledge",
 | 
				
			||||||
 | 
					  "postCategoryLiterature": "Literature",
 | 
				
			||||||
 | 
					  "postCategoryFunny": "Funny",
 | 
				
			||||||
 | 
					  "postCategoryUncategorized": "Uncategorized",
 | 
				
			||||||
 | 
					  "newsAllSources": "All News",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "Swap",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "You're reading from HyperNet.Reader",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "You're reading the original article",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
 | 
				
			||||||
 | 
					  "newsToday": "Today's News",
 | 
				
			||||||
 | 
					  "totpPostSetup": "One More Thing",
 | 
				
			||||||
 | 
					  "totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
 | 
				
			||||||
 | 
					  "totpNeverShare": "Never share this QR Code",
 | 
				
			||||||
 | 
					  "needHelp": "Need Help?",
 | 
				
			||||||
 | 
					  "needHelpLaunch": "Check out our Goatpedia!",
 | 
				
			||||||
 | 
					  "walletCreate": "Create a Wallet",
 | 
				
			||||||
 | 
					  "walletCreateSubtitle": "Create a wallet to start using Source Points",
 | 
				
			||||||
 | 
					  "walletCreatePassword": "Set a payment password for your new wallet below",
 | 
				
			||||||
 | 
					  "walletCurrencyShort": "SRC",
 | 
				
			||||||
 | 
					  "walletCurrency": {
 | 
				
			||||||
 | 
					    "one": "{} Source Point",
 | 
				
			||||||
 | 
					    "other": "{} Source Points"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "aiThinkingProcess": "AI Thinking Process",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "Account settings have been applied.",
 | 
				
			||||||
 | 
					  "trayMenuExit": "Exit",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "Unanswered Question",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "Answered Question",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "Select as Answer",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
 | 
				
			||||||
 | 
					  "postVideoUpload": "Upload Video",
 | 
				
			||||||
 | 
					  "realmJoin": "Join Realm",
 | 
				
			||||||
 | 
					  "realmCommunityHint": "This realm is a community realm, you can freely join.",
 | 
				
			||||||
 | 
					  "realmCommunityPublicChannelsHint": "The public channels in this realm",
 | 
				
			||||||
 | 
					  "realmJoined": "Joined realm {}.",
 | 
				
			||||||
 | 
					  "join": "Join",
 | 
				
			||||||
 | 
					  "pollEditorNew": "New Poll",
 | 
				
			||||||
 | 
					  "pollEditorEdit": "Edit Poll",
 | 
				
			||||||
 | 
					  "pollEditorDelete": "Delete Poll",
 | 
				
			||||||
 | 
					  "pollEditorDeleteDescription": "Are you sure you want to delete this poll? This operation is irreversible.",
 | 
				
			||||||
 | 
					  "pollEditorUnlink": "Unlink Poll",
 | 
				
			||||||
 | 
					  "pollOptionAdd": "Add Option",
 | 
				
			||||||
 | 
					  "pollOptionName": "Option Name",
 | 
				
			||||||
 | 
					  "pollLinkExisting": "Link existing poll",
 | 
				
			||||||
 | 
					  "pollAnswered": "Answered the poll.",
 | 
				
			||||||
 | 
					  "pollVotes": {
 | 
				
			||||||
 | 
					    "one": "{} vote",
 | 
				
			||||||
 | 
					    "other": "{} votes"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,12 +15,17 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "编辑资料",
 | 
					  "screenAccountProfileEdit": "编辑资料",
 | 
				
			||||||
  "screenAbuseReport": "滥用检举",
 | 
					  "screenAbuseReport": "滥用检举",
 | 
				
			||||||
  "screenSettings": "设置",
 | 
					  "screenSettings": "设置",
 | 
				
			||||||
 | 
					  "screenAccountSettings": "账号设置",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "验证因子",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "钱包",
 | 
				
			||||||
 | 
					  "screenNews": "新闻",
 | 
				
			||||||
  "screenAlbum": "相册",
 | 
					  "screenAlbum": "相册",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "编辑聊天频道",
 | 
					  "screenChatManage": "编辑聊天频道",
 | 
				
			||||||
  "screenChatNew": "新建聊天频道",
 | 
					  "screenChatNew": "新建聊天频道",
 | 
				
			||||||
  "screenRealm": "领域",
 | 
					  "screenRealm": "领域",
 | 
				
			||||||
  "screenRealmManage": "编辑领域",
 | 
					  "screenRealmManage": "编辑领域",
 | 
				
			||||||
 | 
					  "screenRealmDiscovery": "发现领域",
 | 
				
			||||||
  "screenRealmNew": "新建领域",
 | 
					  "screenRealmNew": "新建领域",
 | 
				
			||||||
  "screenNotification": "通知",
 | 
					  "screenNotification": "通知",
 | 
				
			||||||
  "screenPostSearch": "搜索帖子",
 | 
					  "screenPostSearch": "搜索帖子",
 | 
				
			||||||
@@ -87,8 +92,18 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "验证代码",
 | 
					  "loginEnterPassword": "验证代码",
 | 
				
			||||||
  "loginSuccess": "登录为 {}",
 | 
					  "loginSuccess": "登录为 {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "删除验证因子",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
 | 
				
			||||||
  "authFactorPassword": "密码",
 | 
					  "authFactorPassword": "密码",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "注册时选择设置的密码。",
 | 
				
			||||||
  "authFactorEmail": "电邮一次性验证码",
 | 
					  "authFactorEmail": "电邮一次性验证码",
 | 
				
			||||||
 | 
					  "authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
 | 
				
			||||||
 | 
					  "authFactorTOTP": "时序验证码",
 | 
				
			||||||
 | 
					  "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
 | 
				
			||||||
 | 
					  "authFactorInAppNotify": "应用内通知验证码",
 | 
				
			||||||
 | 
					  "authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
 | 
				
			||||||
 | 
					  "authFactorAdd": "添加新验证因子",
 | 
				
			||||||
 | 
					  "authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
 | 
				
			||||||
  "accountIntroTitle": "喜欢您来!",
 | 
					  "accountIntroTitle": "喜欢您来!",
 | 
				
			||||||
  "accountIntroSubtitle": "登陆以探索更广大的世界。",
 | 
					  "accountIntroSubtitle": "登陆以探索更广大的世界。",
 | 
				
			||||||
  "accountLogout": "退出登录",
 | 
					  "accountLogout": "退出登录",
 | 
				
			||||||
@@ -97,8 +112,14 @@
 | 
				
			|||||||
  "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
 | 
					  "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
 | 
				
			||||||
  "accountPublishers": "你的发布者",
 | 
					  "accountPublishers": "你的发布者",
 | 
				
			||||||
  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
					  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
				
			||||||
 | 
					  "accountSettings": "帐号设置",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
 | 
				
			||||||
  "accountProfileEdit": "编辑资料",
 | 
					  "accountProfileEdit": "编辑资料",
 | 
				
			||||||
  "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
 | 
					  "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
 | 
				
			||||||
 | 
					  "accountWallet": "钱包",
 | 
				
			||||||
 | 
					  "accountWalletSubtitle": "查看你的余额和交易记录。",
 | 
				
			||||||
 | 
					  "factorSettings": "验证因子",
 | 
				
			||||||
 | 
					  "factorSettingsSubtitle": "管理你的登陆验证方式。",
 | 
				
			||||||
  "accountProfileEditApplied": "个人资料修改已被应用。",
 | 
					  "accountProfileEditApplied": "个人资料修改已被应用。",
 | 
				
			||||||
  "publishersNew": "新发布者",
 | 
					  "publishersNew": "新发布者",
 | 
				
			||||||
  "publisherNewSubtitle": "创建一个新的公共身份。",
 | 
					  "publisherNewSubtitle": "创建一个新的公共身份。",
 | 
				
			||||||
@@ -118,11 +139,17 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
				
			||||||
  "writePostTypeStory": "发动态",
 | 
					  "writePostTypeStory": "发动态",
 | 
				
			||||||
  "writePostTypeArticle": "写文章",
 | 
					  "writePostTypeArticle": "写文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提问题",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "发视频",
 | 
				
			||||||
  "fieldPostPublisher": "帖子发布者",
 | 
					  "fieldPostPublisher": "帖子发布者",
 | 
				
			||||||
  "fieldPostContent": "发生什么事了?!",
 | 
					  "fieldPostContent": "发生什么事了?!",
 | 
				
			||||||
  "fieldPostTitle": "标题",
 | 
					  "fieldPostTitle": "标题",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答奖励源点",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "标签",
 | 
					  "fieldPostTags": "标签",
 | 
				
			||||||
 | 
					  "fieldPostCategories": "分类",
 | 
				
			||||||
 | 
					  "fieldPostAlias": "别名",
 | 
				
			||||||
 | 
					  "fieldPostAliasHint": "可选项,用于在 URL 中表示该帖子,应遵循 URL-Safe 的原则。",
 | 
				
			||||||
  "postPublish": "发布",
 | 
					  "postPublish": "发布",
 | 
				
			||||||
  "postPublishedAt": "发布于",
 | 
					  "postPublishedAt": "发布于",
 | 
				
			||||||
  "postPublishedUntil": "取消发布于",
 | 
					  "postPublishedUntil": "取消发布于",
 | 
				
			||||||
@@ -174,12 +201,30 @@
 | 
				
			|||||||
    "other": "{} 条评论"
 | 
					    "other": "{} 条评论"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "外观",
 | 
					  "settingsAppearance": "外观",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "显示语言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "设置应用程序使用的语言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "跟随系统",
 | 
				
			||||||
  "settingsBackgroundImage": "背景图片",
 | 
					  "settingsBackgroundImage": "背景图片",
 | 
				
			||||||
  "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
 | 
					  "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
 | 
				
			||||||
  "settingsBackgroundImageClear": "清除现存背景图",
 | 
					  "settingsBackgroundImageClear": "清除现存背景图",
 | 
				
			||||||
  "settingsBackgroundImageClearDescription": "将应用背景图重置为空白。",
 | 
					  "settingsBackgroundImageClearDescription": "将应用背景图重置为空白。",
 | 
				
			||||||
  "settingsThemeMaterial3": "使用 Material You 设计范式",
 | 
					  "settingsThemeMaterial3": "使用 Material You 设计范式",
 | 
				
			||||||
  "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
 | 
					  "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
 | 
				
			||||||
 | 
					  "settingsAppBarTransparent": "透明顶栏",
 | 
				
			||||||
 | 
					  "settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
 | 
				
			||||||
 | 
					  "settingsDrawerPreferCollapse": "侧边栏偏好折叠",
 | 
				
			||||||
 | 
					  "settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
 | 
				
			||||||
 | 
					  "settingsColorScheme": "主题色",
 | 
				
			||||||
 | 
					  "settingsColorSchemeDescription": "设置应用主题色。",
 | 
				
			||||||
 | 
					  "settingsColorSeed": "预设色彩主题",
 | 
				
			||||||
 | 
					  "settingsColorSeedDescription": "选择一个预设色彩主题。",
 | 
				
			||||||
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHaptic": "新通知时振动",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展开帖子链接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展开聊天链接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
 | 
				
			||||||
  "settingsNetwork": "网络",
 | 
					  "settingsNetwork": "网络",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服务器",
 | 
					  "settingsNetworkServer": "HyperNet 服务器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
 | 
				
			||||||
@@ -188,15 +233,25 @@
 | 
				
			|||||||
  "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 的版本信息。",
 | 
				
			||||||
 | 
					  "settingsAccountLanguage": "帐号偏好语言",
 | 
				
			||||||
 | 
					  "settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
 | 
				
			||||||
  "sensitiveContent": "敏感内容",
 | 
					  "sensitiveContent": "敏感内容",
 | 
				
			||||||
  "sensitiveContentCollapsed": "敏感内容已折叠。",
 | 
					  "sensitiveContentCollapsed": "敏感内容已折叠。",
 | 
				
			||||||
  "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
 | 
					  "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
 | 
				
			||||||
  "sensitiveContentReveal": "显示内容",
 | 
					  "sensitiveContentReveal": "显示内容",
 | 
				
			||||||
  "serverConnecting": "正在连接服务器…",
 | 
					  "serverConnecting": "正在连接…",
 | 
				
			||||||
  "serverDisconnected": "已与服务器断开连接",
 | 
					  "serverDisconnected": "已断开连接",
 | 
				
			||||||
 | 
					  "serverConnected": "已连接",
 | 
				
			||||||
  "fieldChatAlias": "频道别名",
 | 
					  "fieldChatAlias": "频道别名",
 | 
				
			||||||
  "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
 | 
					  "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
 | 
				
			||||||
  "fieldChatName": "名称",
 | 
					  "fieldChatName": "名称",
 | 
				
			||||||
@@ -263,16 +318,50 @@
 | 
				
			|||||||
    "one": "{} 个附件",
 | 
					    "one": "{} 个附件",
 | 
				
			||||||
    "other": "{} 个附件"
 | 
					    "other": "{} 个附件"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "messageTyping": {
 | 
				
			||||||
 | 
					    "one": "{} 正在输入",
 | 
				
			||||||
 | 
					    "other": "{} 正在输入"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "fieldAttachmentRandomId": "访问 ID",
 | 
				
			||||||
 | 
					  "fieldAttachmentAlt": "概述文字",
 | 
				
			||||||
  "addAttachmentFromAlbum": "从相册中添加附件",
 | 
					  "addAttachmentFromAlbum": "从相册中添加附件",
 | 
				
			||||||
  "addAttachmentFromClipboard": "粘贴附件",
 | 
					  "addAttachmentFromClipboard": "粘贴附件",
 | 
				
			||||||
  "addAttachmentFromCameraPhoto": "拍摄照片",
 | 
					  "addAttachmentFromCameraPhoto": "拍摄照片",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "拍摄视频",
 | 
					  "addAttachmentFromCameraVideo": "拍摄视频",
 | 
				
			||||||
 | 
					  "addAttachmentFromRandomId": "通过访问 ID 链接",
 | 
				
			||||||
 | 
					  "attachmentDetailInfo": "附件详细信息",
 | 
				
			||||||
  "attachmentPastedImage": "粘贴的图片",
 | 
					  "attachmentPastedImage": "粘贴的图片",
 | 
				
			||||||
  "attachmentInsertLink": "插入连接",
 | 
					  "attachmentInsertLink": "插入连接",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
 | 
					  "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
 | 
				
			||||||
  "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
 | 
					  "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
 | 
				
			||||||
 | 
					  "attachmentCompressVideo": "重新编码视频",
 | 
				
			||||||
  "attachmentSetThumbnail": "设置缩略图",
 | 
					  "attachmentSetThumbnail": "设置缩略图",
 | 
				
			||||||
 | 
					  "attachmentSetAlt": "设置概述文字",
 | 
				
			||||||
 | 
					  "attachmentCopyRandomId": "复制访问 ID",
 | 
				
			||||||
  "attachmentUpload": "上传",
 | 
					  "attachmentUpload": "上传",
 | 
				
			||||||
 | 
					  "attachmentInputDialog": "上传附件",
 | 
				
			||||||
 | 
					  "attachmentInputUseRandomId": "使用访问 ID",
 | 
				
			||||||
 | 
					  "attachmentInputNew": "新上传附件",
 | 
				
			||||||
 | 
					  "waitingForUpload": "等待上传",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHint": "压缩一份视频的副本",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHintDescription": "你想上传压缩视频 {} 的副本吗?它将帮助你的观众快速预览视频,并且他们仍然可以观看原始视频。这将会在在你的设备上处理视频,所以需要一些时间,所以请耐心等待。",
 | 
				
			||||||
 | 
					  "attachmentCompressQuality": "压缩质量",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHighest": "最高",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityDefault": "默认",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityMedium": "中等",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityLow": "低",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。",
 | 
				
			||||||
 | 
					  "attachmentUploaded": "已上传",
 | 
				
			||||||
 | 
					  "attachmentPending": "未上传",
 | 
				
			||||||
 | 
					  "attachmentCopyCompressed": "有压缩副本",
 | 
				
			||||||
 | 
					  "attachmentGotBoosted": "有加速传递",
 | 
				
			||||||
 | 
					  "attachmentBoost": "加速包",
 | 
				
			||||||
 | 
					  "attachmentCreateBoost": "加速传递",
 | 
				
			||||||
 | 
					  "attachmentBoostHint": "加速传递允许您将附件上传到更近的受众或更快的内容网络。该功能目前处于 Beta 阶段。该功能限时免费,当有价格计划更改时,您将会被通知。",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegion": "目标节点",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionAPAC": "亚太地区",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionNGB": "中国 · 浙江 · 宁波",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionHKG": "香港",
 | 
				
			||||||
  "notification": "通知",
 | 
					  "notification": "通知",
 | 
				
			||||||
  "notificationUnreadCount": {
 | 
					  "notificationUnreadCount": {
 | 
				
			||||||
    "zero": "无未读通知",
 | 
					    "zero": "无未读通知",
 | 
				
			||||||
@@ -360,7 +449,32 @@
 | 
				
			|||||||
  "dailyCheckNegativeHint5Description": "关键时刻断网",
 | 
					  "dailyCheckNegativeHint5Description": "关键时刻断网",
 | 
				
			||||||
  "dailyCheckNegativeHint6": "出门",
 | 
					  "dailyCheckNegativeHint6": "出门",
 | 
				
			||||||
  "dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
 | 
					  "dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
 | 
				
			||||||
  "happyBirthday": "生日快乐,{}!",
 | 
					  "celebrateBirthday": "生日快乐,{}!",
 | 
				
			||||||
 | 
					  "celebrateLunarNewYear": "春节快乐,{}!",
 | 
				
			||||||
 | 
					  "celebrateMidAutumn": "中秋节快乐,{}!",
 | 
				
			||||||
 | 
					  "celebrateDragonBoat": "端午节快乐,{}!",
 | 
				
			||||||
 | 
					  "celebrateMerryXmas": "圣诞快乐,{}!",
 | 
				
			||||||
 | 
					  "celebrateNewYear": "新年快乐,{}!",
 | 
				
			||||||
 | 
					  "celebrateValentineDay": "今天是情人节,{}!",
 | 
				
			||||||
 | 
					  "celebrateLaborDay": "今天是劳动节,{}。",
 | 
				
			||||||
 | 
					  "celebrateMotherDay": "今天是母亲节,{}。",
 | 
				
			||||||
 | 
					  "celebrateChildrenDay": "今天是儿童节,{}!",
 | 
				
			||||||
 | 
					  "celebrateFatherDay": "今天是父亲节,{}。",
 | 
				
			||||||
 | 
					  "celebrateHalloween": "快乐在圣诞节,{}!",
 | 
				
			||||||
 | 
					  "celebrateThanksgiving": "今天是感恩节,{}!",
 | 
				
			||||||
 | 
					  "pendingLunarNewYear": "{} 过春节",
 | 
				
			||||||
 | 
					  "pendingMidAutumn": "{} 过中秋节",
 | 
				
			||||||
 | 
					  "pendingDragonBoat": "{} 过端午节",
 | 
				
			||||||
 | 
					  "pendingBirthday": "{} 过生日",
 | 
				
			||||||
 | 
					  "pendingMerryXmas": "{} 过圣诞节",
 | 
				
			||||||
 | 
					  "pendingNewYear": "{} 跨年",
 | 
				
			||||||
 | 
					  "pendingValentineDay": "{} 过情人节",
 | 
				
			||||||
 | 
					  "pendingLaborDay": "{} 过劳动节",
 | 
				
			||||||
 | 
					  "pendingMotherDay": "{} 过母亲节",
 | 
				
			||||||
 | 
					  "pendingChildrenDay": "{} 过儿童节",
 | 
				
			||||||
 | 
					  "pendingFatherDay": "{} 过父亲节",
 | 
				
			||||||
 | 
					  "pendingHalloween": "{} 过圣诞节",
 | 
				
			||||||
 | 
					  "pendingThanksgiving": "{} 过感恩节",
 | 
				
			||||||
  "friendNew": "添加好友",
 | 
					  "friendNew": "添加好友",
 | 
				
			||||||
  "friendRequests": "好友请求",
 | 
					  "friendRequests": "好友请求",
 | 
				
			||||||
  "friendRequestsDescription": {
 | 
					  "friendRequestsDescription": {
 | 
				
			||||||
@@ -394,14 +508,16 @@
 | 
				
			|||||||
  "accountJoinedAt": "加入于 {}",
 | 
					  "accountJoinedAt": "加入于 {}",
 | 
				
			||||||
  "accountBirthday": "出生于 {}",
 | 
					  "accountBirthday": "出生于 {}",
 | 
				
			||||||
  "accountBadge": "徽章",
 | 
					  "accountBadge": "徽章",
 | 
				
			||||||
 | 
					  "accountCheckInNoRecords": "暂无运势记录",
 | 
				
			||||||
  "badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
 | 
					  "badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
 | 
				
			||||||
  "badgeSiteMigration": "Solar Network 原住民",
 | 
					  "badgeSiteMigration": "Solar Network 原住民",
 | 
				
			||||||
  "accountStatus": "状态",
 | 
					  "accountStatus": "状态",
 | 
				
			||||||
  "accountStatusOnline": "在线",
 | 
					  "accountStatusOnline": "在线",
 | 
				
			||||||
  "accountStatusOffline": "离线",
 | 
					  "accountStatusOffline": "离线",
 | 
				
			||||||
  "accountStatusLastSeen": "最后一次在 {} 上线",
 | 
					  "accountStatusLastSeen": "最后一次上线于 {}",
 | 
				
			||||||
  "postArticle": "Solar Network 上的文章",
 | 
					  "postArticle": "Solar Network 上的文章",
 | 
				
			||||||
  "postStory": "Solar Network 上的故事",
 | 
					  "postStory": "Solar Network 上的故事",
 | 
				
			||||||
 | 
					  "postLocalDraftRestored": "从本地恢复草稿",
 | 
				
			||||||
  "articleWrittenAt": "发表于 {}",
 | 
					  "articleWrittenAt": "发表于 {}",
 | 
				
			||||||
  "articleEditedAt": "编辑于 {}",
 | 
					  "articleEditedAt": "编辑于 {}",
 | 
				
			||||||
  "attachmentSaved": "已保存到相册",
 | 
					  "attachmentSaved": "已保存到相册",
 | 
				
			||||||
@@ -442,9 +558,84 @@
 | 
				
			|||||||
  "postImageShareAds": "来 Solar Network 探索更多有趣帖子",
 | 
					  "postImageShareAds": "来 Solar Network 探索更多有趣帖子",
 | 
				
			||||||
  "postShare": "分享",
 | 
					  "postShare": "分享",
 | 
				
			||||||
  "postShareImage": "分享帖图",
 | 
					  "postShareImage": "分享帖图",
 | 
				
			||||||
 | 
					  "postGetInsight": "获取见解",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI 见解",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
 | 
				
			||||||
  "appInitializing": "正在初始化",
 | 
					  "appInitializing": "正在初始化",
 | 
				
			||||||
  "poweredBy": "由 {} 提供支持",
 | 
					  "poweredBy": "由 {} 提供支持",
 | 
				
			||||||
  "shareIntent": "分享",
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
  "shareIntentDescription": "您想对您分享的内容做些什么?",
 | 
					  "shareIntentDescription": "您想对您分享的内容做些什么?",
 | 
				
			||||||
  "shareIntentPostStory": "发布动态"
 | 
					  "shareIntentPostStory": "发布动态",
 | 
				
			||||||
 | 
					  "shareIntentSendChannel": "分享到聊天频道",
 | 
				
			||||||
 | 
					  "updateAvailable": "检测到更新可用",
 | 
				
			||||||
 | 
					  "updateOngoing": "正在更新,请稍后……",
 | 
				
			||||||
 | 
					  "custom": "自定义",
 | 
				
			||||||
 | 
					  "colorSchemeIndigo": "靛蓝",
 | 
				
			||||||
 | 
					  "colorSchemeBlue": "蓝色",
 | 
				
			||||||
 | 
					  "colorSchemeGreen": "绿色",
 | 
				
			||||||
 | 
					  "colorSchemeYellow": "黄色",
 | 
				
			||||||
 | 
					  "colorSchemeOrange": "橙色",
 | 
				
			||||||
 | 
					  "colorSchemeRed": "红色",
 | 
				
			||||||
 | 
					  "colorSchemeWhite": "白色",
 | 
				
			||||||
 | 
					  "colorSchemeBlack": "黑色",
 | 
				
			||||||
 | 
					  "colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
 | 
				
			||||||
 | 
					  "postFeaturedComment": "精选评论",
 | 
				
			||||||
 | 
					  "postCategoryTechnology": "技术",
 | 
				
			||||||
 | 
					  "postCategoryGaming": "游戏",
 | 
				
			||||||
 | 
					  "postCategoryLife": "生活",
 | 
				
			||||||
 | 
					  "postCategoryArts": "艺术",
 | 
				
			||||||
 | 
					  "postCategorySports": "体育",
 | 
				
			||||||
 | 
					  "postCategoryMusic": "音乐",
 | 
				
			||||||
 | 
					  "postCategoryNews": "新闻",
 | 
				
			||||||
 | 
					  "postCategoryKnowledge": "知识",
 | 
				
			||||||
 | 
					  "postCategoryLiterature": "文学",
 | 
				
			||||||
 | 
					  "postCategoryFunny": "搞笑",
 | 
				
			||||||
 | 
					  "postCategoryUncategorized": "未分类",
 | 
				
			||||||
 | 
					  "newsAllSources": "所有新闻",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "切换",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "你正在阅读原始文章",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
 | 
				
			||||||
 | 
					  "newsToday": "快讯",
 | 
				
			||||||
 | 
					  "totpPostSetup": "还有一件事",
 | 
				
			||||||
 | 
					  "totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
 | 
				
			||||||
 | 
					  "totpNeverShare": "永远不要分享这个 QR Code",
 | 
				
			||||||
 | 
					  "needHelp": "需要帮助?",
 | 
				
			||||||
 | 
					  "needHelpLaunch": "查看我们的山羊维基!",
 | 
				
			||||||
 | 
					  "walletCreate": "创建钱包",
 | 
				
			||||||
 | 
					  "walletCreateSubtitle": "创建于一个钱包来开始使用源点。",
 | 
				
			||||||
 | 
					  "walletCreatePassword": "在下方设置你的付款密码",
 | 
				
			||||||
 | 
					  "walletCurrencyShort": "源点",
 | 
				
			||||||
 | 
					  "walletCurrency": {
 | 
				
			||||||
 | 
					    "one": "{} 源点",
 | 
				
			||||||
 | 
					    "other": "{} 源点"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "aiThinkingProcess": "AI 思考过程",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "帐号设置已应用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的问题",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的问题",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精选解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "选择解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已选择,奖励已发放。",
 | 
				
			||||||
 | 
					  "postVideoUpload": "上传视频",
 | 
				
			||||||
 | 
					  "realmJoin": "加入领域",
 | 
				
			||||||
 | 
					  "realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
 | 
				
			||||||
 | 
					  "realmCommunityPublicChannelsHint": "该领域包含的公共频道",
 | 
				
			||||||
 | 
					  "realmJoined": "已加入领域 {}。",
 | 
				
			||||||
 | 
					  "join": "加入",
 | 
				
			||||||
 | 
					  "pollEditorNew": "新投票",
 | 
				
			||||||
 | 
					  "pollEditorEdit": "编辑投票",
 | 
				
			||||||
 | 
					  "pollEditorDelete": "删除投票",
 | 
				
			||||||
 | 
					  "pollEditorDeleteDescription": "你确定要删除这个投票吗?该操作不可撤销。",
 | 
				
			||||||
 | 
					  "pollEditorUnlink": "解除链接",
 | 
				
			||||||
 | 
					  "pollOptionAdd": "添加选项",
 | 
				
			||||||
 | 
					  "pollOptionName": "选项名称",
 | 
				
			||||||
 | 
					  "pollLinkExisting": "链接现有投票",
 | 
				
			||||||
 | 
					  "pollAnswered": "答案已经反馈。",
 | 
				
			||||||
 | 
					  "pollVotes": {
 | 
				
			||||||
 | 
					    "one": "{} 票",
 | 
				
			||||||
 | 
					    "other": "{} 票"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,12 +15,17 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "編輯資料",
 | 
					  "screenAccountProfileEdit": "編輯資料",
 | 
				
			||||||
  "screenAbuseReport": "濫用檢舉",
 | 
					  "screenAbuseReport": "濫用檢舉",
 | 
				
			||||||
  "screenSettings": "設置",
 | 
					  "screenSettings": "設置",
 | 
				
			||||||
 | 
					  "screenAccountSettings": "賬號設置",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "錢包",
 | 
				
			||||||
 | 
					  "screenNews": "新聞",
 | 
				
			||||||
  "screenAlbum": "相冊",
 | 
					  "screenAlbum": "相冊",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "編輯聊天頻道",
 | 
					  "screenChatManage": "編輯聊天頻道",
 | 
				
			||||||
  "screenChatNew": "新建聊天頻道",
 | 
					  "screenChatNew": "新建聊天頻道",
 | 
				
			||||||
  "screenRealm": "領域",
 | 
					  "screenRealm": "領域",
 | 
				
			||||||
  "screenRealmManage": "編輯領域",
 | 
					  "screenRealmManage": "編輯領域",
 | 
				
			||||||
 | 
					  "screenRealmDiscovery": "發現領域",
 | 
				
			||||||
  "screenRealmNew": "新建領域",
 | 
					  "screenRealmNew": "新建領域",
 | 
				
			||||||
  "screenNotification": "通知",
 | 
					  "screenNotification": "通知",
 | 
				
			||||||
  "screenPostSearch": "搜索帖子",
 | 
					  "screenPostSearch": "搜索帖子",
 | 
				
			||||||
@@ -87,8 +92,18 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "驗證代碼",
 | 
					  "loginEnterPassword": "驗證代碼",
 | 
				
			||||||
  "loginSuccess": "登錄為 {}",
 | 
					  "loginSuccess": "登錄為 {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "刪除驗證因子",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
 | 
				
			||||||
  "authFactorPassword": "密碼",
 | 
					  "authFactorPassword": "密碼",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "註冊時選擇設置的密碼。",
 | 
				
			||||||
  "authFactorEmail": "電郵一次性驗證碼",
 | 
					  "authFactorEmail": "電郵一次性驗證碼",
 | 
				
			||||||
 | 
					  "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorTOTP": "時序驗證碼",
 | 
				
			||||||
 | 
					  "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorInAppNotify": "應用內通知驗證碼",
 | 
				
			||||||
 | 
					  "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorAdd": "添加新驗證因子",
 | 
				
			||||||
 | 
					  "authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
 | 
				
			||||||
  "accountIntroTitle": "喜歡您來!",
 | 
					  "accountIntroTitle": "喜歡您來!",
 | 
				
			||||||
  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
					  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
				
			||||||
  "accountLogout": "退出登錄",
 | 
					  "accountLogout": "退出登錄",
 | 
				
			||||||
@@ -97,8 +112,14 @@
 | 
				
			|||||||
  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
					  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
				
			||||||
  "accountPublishers": "你的發佈者",
 | 
					  "accountPublishers": "你的發佈者",
 | 
				
			||||||
  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
					  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
				
			||||||
 | 
					  "accountSettings": "帳號設置",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
 | 
				
			||||||
  "accountProfileEdit": "編輯資料",
 | 
					  "accountProfileEdit": "編輯資料",
 | 
				
			||||||
  "accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
 | 
					  "accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
 | 
				
			||||||
 | 
					  "accountWallet": "錢包",
 | 
				
			||||||
 | 
					  "accountWalletSubtitle": "查看你的餘額和交易記錄。",
 | 
				
			||||||
 | 
					  "factorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "factorSettingsSubtitle": "管理你的登陸驗證方式。",
 | 
				
			||||||
  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
					  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
				
			||||||
  "publishersNew": "新發布者",
 | 
					  "publishersNew": "新發布者",
 | 
				
			||||||
  "publisherNewSubtitle": "創建一個新的公共身份。",
 | 
					  "publisherNewSubtitle": "創建一個新的公共身份。",
 | 
				
			||||||
@@ -118,11 +139,17 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
				
			||||||
  "writePostTypeStory": "發動態",
 | 
					  "writePostTypeStory": "發動態",
 | 
				
			||||||
  "writePostTypeArticle": "寫文章",
 | 
					  "writePostTypeArticle": "寫文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提問題",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "發視頻",
 | 
				
			||||||
  "fieldPostPublisher": "帖子發佈者",
 | 
					  "fieldPostPublisher": "帖子發佈者",
 | 
				
			||||||
  "fieldPostContent": "發生什麼事了?!",
 | 
					  "fieldPostContent": "發生什麼事了?!",
 | 
				
			||||||
  "fieldPostTitle": "標題",
 | 
					  "fieldPostTitle": "標題",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答獎勵源點",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "標籤",
 | 
					  "fieldPostTags": "標籤",
 | 
				
			||||||
 | 
					  "fieldPostCategories": "分類",
 | 
				
			||||||
 | 
					  "fieldPostAlias": "別名",
 | 
				
			||||||
 | 
					  "fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
 | 
				
			||||||
  "postPublish": "發佈",
 | 
					  "postPublish": "發佈",
 | 
				
			||||||
  "postPublishedAt": "發佈於",
 | 
					  "postPublishedAt": "發佈於",
 | 
				
			||||||
  "postPublishedUntil": "取消發佈於",
 | 
					  "postPublishedUntil": "取消發佈於",
 | 
				
			||||||
@@ -174,12 +201,30 @@
 | 
				
			|||||||
    "other": "{} 條評論"
 | 
					    "other": "{} 條評論"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "外觀",
 | 
					  "settingsAppearance": "外觀",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "顯示語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "跟隨系統",
 | 
				
			||||||
  "settingsBackgroundImage": "背景圖片",
 | 
					  "settingsBackgroundImage": "背景圖片",
 | 
				
			||||||
  "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
 | 
					  "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
 | 
				
			||||||
  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
					  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
				
			||||||
  "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
 | 
					  "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
 | 
				
			||||||
  "settingsThemeMaterial3": "使用 Material You 設計範式",
 | 
					  "settingsThemeMaterial3": "使用 Material You 設計範式",
 | 
				
			||||||
  "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
 | 
					  "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
 | 
				
			||||||
 | 
					  "settingsAppBarTransparent": "透明頂欄",
 | 
				
			||||||
 | 
					  "settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
 | 
				
			||||||
 | 
					  "settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
 | 
				
			||||||
 | 
					  "settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
 | 
				
			||||||
 | 
					  "settingsColorScheme": "主題色",
 | 
				
			||||||
 | 
					  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
				
			||||||
 | 
					  "settingsColorSeed": "預設色彩主題",
 | 
				
			||||||
 | 
					  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
				
			||||||
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHaptic": "新通知時振動",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展開帖子鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展開聊天鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
				
			||||||
  "settingsNetwork": "網絡",
 | 
					  "settingsNetwork": "網絡",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服務器",
 | 
					  "settingsNetworkServer": "HyperNet 服務器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
				
			||||||
@@ -188,15 +233,25 @@
 | 
				
			|||||||
  "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 的版本信息。",
 | 
				
			||||||
 | 
					  "settingsAccountLanguage": "帳號偏好語言",
 | 
				
			||||||
 | 
					  "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
 | 
				
			||||||
  "sensitiveContent": "敏感內容",
 | 
					  "sensitiveContent": "敏感內容",
 | 
				
			||||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
					  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
				
			||||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
					  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
				
			||||||
  "sensitiveContentReveal": "顯示內容",
 | 
					  "sensitiveContentReveal": "顯示內容",
 | 
				
			||||||
  "serverConnecting": "正在連接服務器…",
 | 
					  "serverConnecting": "正在連接…",
 | 
				
			||||||
  "serverDisconnected": "已與服務器斷開連接",
 | 
					  "serverDisconnected": "已斷開連接",
 | 
				
			||||||
 | 
					  "serverConnected": "已連接",
 | 
				
			||||||
  "fieldChatAlias": "頻道別名",
 | 
					  "fieldChatAlias": "頻道別名",
 | 
				
			||||||
  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
					  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
				
			||||||
  "fieldChatName": "名稱",
 | 
					  "fieldChatName": "名稱",
 | 
				
			||||||
@@ -263,16 +318,50 @@
 | 
				
			|||||||
    "one": "{} 個附件",
 | 
					    "one": "{} 個附件",
 | 
				
			||||||
    "other": "{} 個附件"
 | 
					    "other": "{} 個附件"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "messageTyping": {
 | 
				
			||||||
 | 
					    "one": "{} 正在輸入",
 | 
				
			||||||
 | 
					    "other": "{} 正在輸入"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "fieldAttachmentRandomId": "訪問 ID",
 | 
				
			||||||
 | 
					  "fieldAttachmentAlt": "概述文字",
 | 
				
			||||||
  "addAttachmentFromAlbum": "從相冊中添加附件",
 | 
					  "addAttachmentFromAlbum": "從相冊中添加附件",
 | 
				
			||||||
  "addAttachmentFromClipboard": "粘貼附件",
 | 
					  "addAttachmentFromClipboard": "粘貼附件",
 | 
				
			||||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
					  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
					  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
				
			||||||
 | 
					  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
				
			||||||
 | 
					  "attachmentDetailInfo": "附件詳細信息",
 | 
				
			||||||
  "attachmentPastedImage": "粘貼的圖片",
 | 
					  "attachmentPastedImage": "粘貼的圖片",
 | 
				
			||||||
  "attachmentInsertLink": "插入連接",
 | 
					  "attachmentInsertLink": "插入連接",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
					  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
				
			||||||
  "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
 | 
					  "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
 | 
				
			||||||
 | 
					  "attachmentCompressVideo": "重新編碼視頻",
 | 
				
			||||||
  "attachmentSetThumbnail": "設置縮略圖",
 | 
					  "attachmentSetThumbnail": "設置縮略圖",
 | 
				
			||||||
 | 
					  "attachmentSetAlt": "設置概述文字",
 | 
				
			||||||
 | 
					  "attachmentCopyRandomId": "複製訪問 ID",
 | 
				
			||||||
  "attachmentUpload": "上傳",
 | 
					  "attachmentUpload": "上傳",
 | 
				
			||||||
 | 
					  "attachmentInputDialog": "上傳附件",
 | 
				
			||||||
 | 
					  "attachmentInputUseRandomId": "使用訪問 ID",
 | 
				
			||||||
 | 
					  "attachmentInputNew": "新上傳附件",
 | 
				
			||||||
 | 
					  "waitingForUpload": "等待上傳",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHint": "壓縮一份視頻的副本",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
 | 
				
			||||||
 | 
					  "attachmentCompressQuality": "壓縮質量",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHighest": "最高",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityDefault": "默認",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityMedium": "中等",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityLow": "低",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
 | 
				
			||||||
 | 
					  "attachmentUploaded": "已上傳",
 | 
				
			||||||
 | 
					  "attachmentPending": "未上傳",
 | 
				
			||||||
 | 
					  "attachmentCopyCompressed": "有壓縮副本",
 | 
				
			||||||
 | 
					  "attachmentGotBoosted": "有加速傳遞",
 | 
				
			||||||
 | 
					  "attachmentBoost": "加速包",
 | 
				
			||||||
 | 
					  "attachmentCreateBoost": "加速傳遞",
 | 
				
			||||||
 | 
					  "attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegion": "目標節點",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionAPAC": "亞太地區",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionHKG": "香港",
 | 
				
			||||||
  "notification": "通知",
 | 
					  "notification": "通知",
 | 
				
			||||||
  "notificationUnreadCount": {
 | 
					  "notificationUnreadCount": {
 | 
				
			||||||
    "zero": "無未讀通知",
 | 
					    "zero": "無未讀通知",
 | 
				
			||||||
@@ -360,7 +449,32 @@
 | 
				
			|||||||
  "dailyCheckNegativeHint5Description": "關鍵時刻斷網",
 | 
					  "dailyCheckNegativeHint5Description": "關鍵時刻斷網",
 | 
				
			||||||
  "dailyCheckNegativeHint6": "出門",
 | 
					  "dailyCheckNegativeHint6": "出門",
 | 
				
			||||||
  "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
 | 
					  "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
 | 
				
			||||||
  "happyBirthday": "生日快樂,{}!",
 | 
					  "celebrateBirthday": "生日快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateLunarNewYear": "春節快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateMidAutumn": "中秋節快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateDragonBoat": "端午節快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateMerryXmas": "聖誕快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateNewYear": "新年快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateValentineDay": "今天是情人節,{}!",
 | 
				
			||||||
 | 
					  "celebrateLaborDay": "今天是勞動節,{}。",
 | 
				
			||||||
 | 
					  "celebrateMotherDay": "今天是母親節,{}。",
 | 
				
			||||||
 | 
					  "celebrateChildrenDay": "今天是兒童節,{}!",
 | 
				
			||||||
 | 
					  "celebrateFatherDay": "今天是父親節,{}。",
 | 
				
			||||||
 | 
					  "celebrateHalloween": "快樂在聖誕節,{}!",
 | 
				
			||||||
 | 
					  "celebrateThanksgiving": "今天是感恩節,{}!",
 | 
				
			||||||
 | 
					  "pendingLunarNewYear": "{} 過春節",
 | 
				
			||||||
 | 
					  "pendingMidAutumn": "{} 過中秋節",
 | 
				
			||||||
 | 
					  "pendingDragonBoat": "{} 過端午節",
 | 
				
			||||||
 | 
					  "pendingBirthday": "{} 過生日",
 | 
				
			||||||
 | 
					  "pendingMerryXmas": "{} 過聖誕節",
 | 
				
			||||||
 | 
					  "pendingNewYear": "{} 跨年",
 | 
				
			||||||
 | 
					  "pendingValentineDay": "{} 過情人節",
 | 
				
			||||||
 | 
					  "pendingLaborDay": "{} 過勞動節",
 | 
				
			||||||
 | 
					  "pendingMotherDay": "{} 過母親節",
 | 
				
			||||||
 | 
					  "pendingChildrenDay": "{} 過兒童節",
 | 
				
			||||||
 | 
					  "pendingFatherDay": "{} 過父親節",
 | 
				
			||||||
 | 
					  "pendingHalloween": "{} 過聖誕節",
 | 
				
			||||||
 | 
					  "pendingThanksgiving": "{} 過感恩節",
 | 
				
			||||||
  "friendNew": "添加好友",
 | 
					  "friendNew": "添加好友",
 | 
				
			||||||
  "friendRequests": "好友請求",
 | 
					  "friendRequests": "好友請求",
 | 
				
			||||||
  "friendRequestsDescription": {
 | 
					  "friendRequestsDescription": {
 | 
				
			||||||
@@ -394,14 +508,16 @@
 | 
				
			|||||||
  "accountJoinedAt": "加入於 {}",
 | 
					  "accountJoinedAt": "加入於 {}",
 | 
				
			||||||
  "accountBirthday": "出生於 {}",
 | 
					  "accountBirthday": "出生於 {}",
 | 
				
			||||||
  "accountBadge": "徽章",
 | 
					  "accountBadge": "徽章",
 | 
				
			||||||
 | 
					  "accountCheckInNoRecords": "暫無運勢記錄",
 | 
				
			||||||
  "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
 | 
					  "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
 | 
				
			||||||
  "badgeSiteMigration": "Solar Network 原住民",
 | 
					  "badgeSiteMigration": "Solar Network 原住民",
 | 
				
			||||||
  "accountStatus": "狀態",
 | 
					  "accountStatus": "狀態",
 | 
				
			||||||
  "accountStatusOnline": "在線",
 | 
					  "accountStatusOnline": "在線",
 | 
				
			||||||
  "accountStatusOffline": "離線",
 | 
					  "accountStatusOffline": "離線",
 | 
				
			||||||
  "accountStatusLastSeen": "最後一次在 {} 上線",
 | 
					  "accountStatusLastSeen": "最後一次上線於 {}",
 | 
				
			||||||
  "postArticle": "Solar Network 上的文章",
 | 
					  "postArticle": "Solar Network 上的文章",
 | 
				
			||||||
  "postStory": "Solar Network 上的故事",
 | 
					  "postStory": "Solar Network 上的故事",
 | 
				
			||||||
 | 
					  "postLocalDraftRestored": "從本地恢復草稿",
 | 
				
			||||||
  "articleWrittenAt": "發表於 {}",
 | 
					  "articleWrittenAt": "發表於 {}",
 | 
				
			||||||
  "articleEditedAt": "編輯於 {}",
 | 
					  "articleEditedAt": "編輯於 {}",
 | 
				
			||||||
  "attachmentSaved": "已保存到相冊",
 | 
					  "attachmentSaved": "已保存到相冊",
 | 
				
			||||||
@@ -441,5 +557,72 @@
 | 
				
			|||||||
  "postImageShareReadMore": "掃描右側 QRCode 查看全文",
 | 
					  "postImageShareReadMore": "掃描右側 QRCode 查看全文",
 | 
				
			||||||
  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
					  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
				
			||||||
  "postShare": "分享",
 | 
					  "postShare": "分享",
 | 
				
			||||||
  "postShareImage": "分享帖圖"
 | 
					  "postShareImage": "分享帖圖",
 | 
				
			||||||
 | 
					  "postGetInsight": "獲取見解",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI 見解",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
 | 
				
			||||||
 | 
					  "appInitializing": "正在初始化",
 | 
				
			||||||
 | 
					  "poweredBy": "由 {} 提供支持",
 | 
				
			||||||
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
 | 
					  "shareIntentDescription": "您想對您分享的內容做些什麼?",
 | 
				
			||||||
 | 
					  "shareIntentPostStory": "發佈動態",
 | 
				
			||||||
 | 
					  "shareIntentSendChannel": "分享到聊天頻道",
 | 
				
			||||||
 | 
					  "updateAvailable": "檢測到更新可用",
 | 
				
			||||||
 | 
					  "updateOngoing": "正在更新,請稍後……",
 | 
				
			||||||
 | 
					  "custom": "自定義",
 | 
				
			||||||
 | 
					  "colorSchemeIndigo": "靛藍",
 | 
				
			||||||
 | 
					  "colorSchemeBlue": "藍色",
 | 
				
			||||||
 | 
					  "colorSchemeGreen": "綠色",
 | 
				
			||||||
 | 
					  "colorSchemeYellow": "黃色",
 | 
				
			||||||
 | 
					  "colorSchemeOrange": "橙色",
 | 
				
			||||||
 | 
					  "colorSchemeRed": "紅色",
 | 
				
			||||||
 | 
					  "colorSchemeWhite": "白色",
 | 
				
			||||||
 | 
					  "colorSchemeBlack": "黑色",
 | 
				
			||||||
 | 
					  "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
 | 
				
			||||||
 | 
					  "postFeaturedComment": "精選評論",
 | 
				
			||||||
 | 
					  "postCategoryTechnology": "技術",
 | 
				
			||||||
 | 
					  "postCategoryGaming": "遊戲",
 | 
				
			||||||
 | 
					  "postCategoryLife": "生活",
 | 
				
			||||||
 | 
					  "postCategoryArts": "藝術",
 | 
				
			||||||
 | 
					  "postCategorySports": "體育",
 | 
				
			||||||
 | 
					  "postCategoryMusic": "音樂",
 | 
				
			||||||
 | 
					  "postCategoryNews": "新聞",
 | 
				
			||||||
 | 
					  "postCategoryKnowledge": "知識",
 | 
				
			||||||
 | 
					  "postCategoryLiterature": "文學",
 | 
				
			||||||
 | 
					  "postCategoryFunny": "搞笑",
 | 
				
			||||||
 | 
					  "postCategoryUncategorized": "未分類",
 | 
				
			||||||
 | 
					  "newsAllSources": "所有新聞",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "切換",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "你正在閲讀原始文章",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
 | 
				
			||||||
 | 
					  "newsToday": "快訊",
 | 
				
			||||||
 | 
					  "totpPostSetup": "還有一件事",
 | 
				
			||||||
 | 
					  "totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
 | 
				
			||||||
 | 
					  "totpNeverShare": "永遠不要分享這個 QR Code",
 | 
				
			||||||
 | 
					  "needHelp": "需要幫助?",
 | 
				
			||||||
 | 
					  "needHelpLaunch": "查看我們的山羊維基!",
 | 
				
			||||||
 | 
					  "walletCreate": "創建錢包",
 | 
				
			||||||
 | 
					  "walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
 | 
				
			||||||
 | 
					  "walletCreatePassword": "在下方設置你的付款密碼",
 | 
				
			||||||
 | 
					  "walletCurrencyShort": "源點",
 | 
				
			||||||
 | 
					  "walletCurrency": {
 | 
				
			||||||
 | 
					    "one": "{} 源點",
 | 
				
			||||||
 | 
					    "other": "{} 源點"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "aiThinkingProcess": "AI 思考過程",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "帳號設置已應用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精選解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "選擇解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
 | 
				
			||||||
 | 
					  "postVideoUpload": "上傳視頻",
 | 
				
			||||||
 | 
					  "realmJoin": "加入領域",
 | 
				
			||||||
 | 
					  "realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
 | 
				
			||||||
 | 
					  "realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
 | 
				
			||||||
 | 
					  "realmJoined": "已加入領域 {}。",
 | 
				
			||||||
 | 
					  "join": "加入"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,53 +7,58 @@
 | 
				
			|||||||
  "screenAuthLogin": "登陸",
 | 
					  "screenAuthLogin": "登陸",
 | 
				
			||||||
  "screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
 | 
					  "screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
 | 
				
			||||||
  "screenAuthLoginGreeting": "歡迎回來",
 | 
					  "screenAuthLoginGreeting": "歡迎回來",
 | 
				
			||||||
  "screenAuthRegister": "建立賬號",
 | 
					  "screenAuthRegister": "創建賬號",
 | 
				
			||||||
  "screenAuthRegisterSubtitle": "建立一個 Solarpass 賬號",
 | 
					  "screenAuthRegisterSubtitle": "創建一個 Solarpass 賬號",
 | 
				
			||||||
  "screenAccountPublishers": "釋出者",
 | 
					  "screenAccountPublishers": "發佈者",
 | 
				
			||||||
  "screenAccountPublisherNew": "新建釋出者",
 | 
					  "screenAccountPublisherNew": "新建發佈者",
 | 
				
			||||||
  "screenAccountPublisherEdit": "編輯釋出者",
 | 
					  "screenAccountPublisherEdit": "編輯發佈者",
 | 
				
			||||||
  "screenAccountProfileEdit": "編輯資料",
 | 
					  "screenAccountProfileEdit": "編輯資料",
 | 
				
			||||||
  "screenAbuseReport": "濫用檢舉",
 | 
					  "screenAbuseReport": "濫用檢舉",
 | 
				
			||||||
  "screenSettings": "設定",
 | 
					  "screenSettings": "設置",
 | 
				
			||||||
  "screenAlbum": "相簿",
 | 
					  "screenAccountSettings": "賬號設置",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "錢包",
 | 
				
			||||||
 | 
					  "screenNews": "新聞",
 | 
				
			||||||
 | 
					  "screenAlbum": "相冊",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "編輯聊天頻道",
 | 
					  "screenChatManage": "編輯聊天頻道",
 | 
				
			||||||
  "screenChatNew": "新建聊天頻道",
 | 
					  "screenChatNew": "新建聊天頻道",
 | 
				
			||||||
  "screenRealm": "領域",
 | 
					  "screenRealm": "領域",
 | 
				
			||||||
  "screenRealmManage": "編輯領域",
 | 
					  "screenRealmManage": "編輯領域",
 | 
				
			||||||
 | 
					  "screenRealmDiscovery": "發現領域",
 | 
				
			||||||
  "screenRealmNew": "新建領域",
 | 
					  "screenRealmNew": "新建領域",
 | 
				
			||||||
  "screenNotification": "通知",
 | 
					  "screenNotification": "通知",
 | 
				
			||||||
  "screenPostSearch": "搜尋帖子",
 | 
					  "screenPostSearch": "搜索帖子",
 | 
				
			||||||
  "screenFriend": "好友",
 | 
					  "screenFriend": "好友",
 | 
				
			||||||
  "dialogOkay": "好的",
 | 
					  "dialogOkay": "好的",
 | 
				
			||||||
  "dialogCancel": "取消",
 | 
					  "dialogCancel": "取消",
 | 
				
			||||||
  "dialogConfirm": "確認",
 | 
					  "dialogConfirm": "確認",
 | 
				
			||||||
  "dialogDismiss": "忽略",
 | 
					  "dialogDismiss": "忽略",
 | 
				
			||||||
  "dialogError": "出了點問題",
 | 
					  "dialogError": "出了點問題",
 | 
				
			||||||
  "errorRequestBad": "伺服器拒絕了您的請求,請檢查您的輸入。",
 | 
					  "errorRequestBad": "服務器拒絕了您的請求,請檢查您的輸入。",
 | 
				
			||||||
  "errorRequestUnauthorized": "未授權的請求,請登入或者嘗試重新登陸。",
 | 
					  "errorRequestUnauthorized": "未授權的請求,請登錄或者嘗試重新登陸。",
 | 
				
			||||||
  "errorRequestForbidden": "被禁止的請求,您沒有足夠的許可權去做那件事。",
 | 
					  "errorRequestForbidden": "被禁止的請求,您沒有足夠的權限去做那件事。",
 | 
				
			||||||
  "errorRequestNotFound": "您正查詢的資源無法被找到。",
 | 
					  "errorRequestNotFound": "您正查找的資源無法被找到。",
 | 
				
			||||||
  "errorRequestConnection": "網路連線錯誤,請檢查您的網路狀態或者檢查我們的服務狀態。",
 | 
					  "errorRequestConnection": "網絡連接錯誤,請檢查您的網絡狀態或者檢查我們的服務狀態。",
 | 
				
			||||||
  "errorRequestUnknown": "未知請求錯誤,您可能想將此對話方塊截圖併發送給我們。",
 | 
					  "errorRequestUnknown": "未知請求錯誤,您可能想將此對話框截圖併發送給我們。",
 | 
				
			||||||
  "unknown": "未知",
 | 
					  "unknown": "未知",
 | 
				
			||||||
  "loading": "載入中…",
 | 
					  "loading": "加載中…",
 | 
				
			||||||
  "prev": "上一步",
 | 
					  "prev": "上一步",
 | 
				
			||||||
  "next": "下一步",
 | 
					  "next": "下一步",
 | 
				
			||||||
  "edit": "編輯",
 | 
					  "edit": "編輯",
 | 
				
			||||||
  "apply": "應用",
 | 
					  "apply": "應用",
 | 
				
			||||||
  "cancel": "取消",
 | 
					  "cancel": "取消",
 | 
				
			||||||
  "create": "建立",
 | 
					  "create": "創建",
 | 
				
			||||||
  "preview": "預覽",
 | 
					  "preview": "預覽",
 | 
				
			||||||
  "delete": "刪除",
 | 
					  "delete": "刪除",
 | 
				
			||||||
  "unlink": "解除連結",
 | 
					  "unlink": "解除鏈接",
 | 
				
			||||||
  "crop": "裁剪",
 | 
					  "crop": "裁剪",
 | 
				
			||||||
  "compress": "壓縮",
 | 
					  "compress": "壓縮",
 | 
				
			||||||
  "report": "檢舉",
 | 
					  "report": "檢舉",
 | 
				
			||||||
  "repost": "轉帖",
 | 
					  "repost": "轉帖",
 | 
				
			||||||
  "replyPost": "回貼",
 | 
					  "replyPost": "回貼",
 | 
				
			||||||
  "reply": "回覆",
 | 
					  "reply": "回覆",
 | 
				
			||||||
  "unset": "未設定",
 | 
					  "unset": "未設置",
 | 
				
			||||||
  "untitled": "無題",
 | 
					  "untitled": "無題",
 | 
				
			||||||
  "postDetail": "帖子詳情",
 | 
					  "postDetail": "帖子詳情",
 | 
				
			||||||
  "postNoun": "帖子",
 | 
					  "postNoun": "帖子",
 | 
				
			||||||
@@ -64,20 +69,20 @@
 | 
				
			|||||||
    "one": "總計 {} 字",
 | 
					    "one": "總計 {} 字",
 | 
				
			||||||
    "other": "總計 {} 字"
 | 
					    "other": "總計 {} 字"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "fieldUsername": "使用者名稱",
 | 
					  "fieldUsername": "用戶名",
 | 
				
			||||||
  "fieldNickname": "顯示名",
 | 
					  "fieldNickname": "顯示名",
 | 
				
			||||||
  "fieldEmail": "電子郵箱地址",
 | 
					  "fieldEmail": "電子郵箱地址",
 | 
				
			||||||
  "fieldPassword": "密碼",
 | 
					  "fieldPassword": "密碼",
 | 
				
			||||||
  "fieldUsernameAlphanumOnly": "使用者名稱只能包含英文大小寫字母和數字。",
 | 
					  "fieldUsernameAlphanumOnly": "用戶名只能包含英文大小寫字母和數字。",
 | 
				
			||||||
  "fieldUsernameLengthLimit": "使用者名稱必須在 {} 和 {} 之間。",
 | 
					  "fieldUsernameLengthLimit": "用戶名必須在 {} 和 {} 之間。",
 | 
				
			||||||
  "fieldUsernameCannotEditHint": "使用者名稱在建立後無法修改",
 | 
					  "fieldUsernameCannotEditHint": "用戶名在創建後無法修改",
 | 
				
			||||||
  "fieldUsernameLookupHint": "支援使用者名稱、電話號碼或郵箱地址",
 | 
					  "fieldUsernameLookupHint": "支持用戶名、電話號碼或郵箱地址",
 | 
				
			||||||
  "fieldNicknameLengthLimit": "暱稱必須在 {} 和 {} 之間。",
 | 
					  "fieldNicknameLengthLimit": "暱稱必須在 {} 和 {} 之間。",
 | 
				
			||||||
  "fieldEmailAddressMustBeValid": "電子郵箱地址必須是一個電子郵箱地址。",
 | 
					  "fieldEmailAddressMustBeValid": "電子郵箱地址必須是一個電子郵箱地址。",
 | 
				
			||||||
  "fieldFirstName": "名",
 | 
					  "fieldFirstName": "名",
 | 
				
			||||||
  "fieldLastName": "姓",
 | 
					  "fieldLastName": "姓",
 | 
				
			||||||
  "fieldBirthday": "生日",
 | 
					  "fieldBirthday": "生日",
 | 
				
			||||||
  "fieldImageHint": "你可以點選這些個人頭像來編輯它們。",
 | 
					  "fieldImageHint": "你可以點擊這些個人頭像來編輯它們。",
 | 
				
			||||||
  "fieldDescription": "簡介",
 | 
					  "fieldDescription": "簡介",
 | 
				
			||||||
  "forgotPassword": "忘記密碼",
 | 
					  "forgotPassword": "忘記密碼",
 | 
				
			||||||
  "loginPickFactor": "選擇方式驗證",
 | 
					  "loginPickFactor": "選擇方式驗證",
 | 
				
			||||||
@@ -85,24 +90,40 @@
 | 
				
			|||||||
    "one": "{} 步驗證",
 | 
					    "one": "{} 步驗證",
 | 
				
			||||||
    "other": "{} 步驗證"
 | 
					    "other": "{} 步驗證"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "驗證程式碼",
 | 
					  "loginEnterPassword": "驗證代碼",
 | 
				
			||||||
  "loginSuccess": "登入為 {}",
 | 
					  "loginSuccess": "登錄為 {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "刪除驗證因子",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
 | 
				
			||||||
  "authFactorPassword": "密碼",
 | 
					  "authFactorPassword": "密碼",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "註冊時選擇設置的密碼。",
 | 
				
			||||||
  "authFactorEmail": "電郵一次性驗證碼",
 | 
					  "authFactorEmail": "電郵一次性驗證碼",
 | 
				
			||||||
 | 
					  "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorTOTP": "時序驗證碼",
 | 
				
			||||||
 | 
					  "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorInAppNotify": "應用內通知驗證碼",
 | 
				
			||||||
 | 
					  "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorAdd": "添加新驗證因子",
 | 
				
			||||||
 | 
					  "authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
 | 
				
			||||||
  "accountIntroTitle": "喜歡您來!",
 | 
					  "accountIntroTitle": "喜歡您來!",
 | 
				
			||||||
  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
					  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
				
			||||||
  "accountLogout": "退出登入",
 | 
					  "accountLogout": "退出登錄",
 | 
				
			||||||
  "accountLogoutSubtitle": "登出當前賬戶的登陸狀態。",
 | 
					  "accountLogoutSubtitle": "註銷當前賬戶的登陸狀態。",
 | 
				
			||||||
  "accountLogoutConfirmTitle": "您確定要退出登入嗎?",
 | 
					  "accountLogoutConfirmTitle": "您確定要退出登錄嗎?",
 | 
				
			||||||
  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
					  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
				
			||||||
  "accountPublishers": "你的釋出者",
 | 
					  "accountPublishers": "你的發佈者",
 | 
				
			||||||
  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
					  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
				
			||||||
 | 
					  "accountSettings": "帳號設置",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
 | 
				
			||||||
  "accountProfileEdit": "編輯資料",
 | 
					  "accountProfileEdit": "編輯資料",
 | 
				
			||||||
  "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
 | 
					  "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
 | 
				
			||||||
 | 
					  "accountWallet": "錢包",
 | 
				
			||||||
 | 
					  "accountWalletSubtitle": "查看你的餘額和交易記錄。",
 | 
				
			||||||
 | 
					  "factorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "factorSettingsSubtitle": "管理你的登陸驗證方式。",
 | 
				
			||||||
  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
					  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
				
			||||||
  "publishersNew": "新發布者",
 | 
					  "publishersNew": "新發布者",
 | 
				
			||||||
  "publisherNewSubtitle": "建立一個新的公共身份。",
 | 
					  "publisherNewSubtitle": "創建一個新的公共身份。",
 | 
				
			||||||
  "publisherSyncWithAccount": "同步賬戶資訊",
 | 
					  "publisherSyncWithAccount": "同步賬戶信息",
 | 
				
			||||||
  "publisherTotalUpvote": "總頂數",
 | 
					  "publisherTotalUpvote": "總頂數",
 | 
				
			||||||
  "publisherTotalDownvote": "總踩數",
 | 
					  "publisherTotalDownvote": "總踩數",
 | 
				
			||||||
  "publisherSocialPoint": "社會信用點",
 | 
					  "publisherSocialPoint": "社會信用點",
 | 
				
			||||||
@@ -115,34 +136,40 @@
 | 
				
			|||||||
  "publisherAffiliatedBy": "隸屬於 {}",
 | 
					  "publisherAffiliatedBy": "隸屬於 {}",
 | 
				
			||||||
  "publisherRunBy": "由 {} 管理",
 | 
					  "publisherRunBy": "由 {} 管理",
 | 
				
			||||||
  "fieldPublisherBelongToRealm": "所屬領域",
 | 
					  "fieldPublisherBelongToRealm": "所屬領域",
 | 
				
			||||||
  "fieldPublisherBelongToRealmUnset": "未設定釋出者所屬領域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
				
			||||||
  "writePostTypeStory": "發動態",
 | 
					  "writePostTypeStory": "發動態",
 | 
				
			||||||
  "writePostTypeArticle": "寫文章",
 | 
					  "writePostTypeArticle": "寫文章",
 | 
				
			||||||
  "fieldPostPublisher": "帖子釋出者",
 | 
					  "writePostTypeQuestion": "提問題",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "發視頻",
 | 
				
			||||||
 | 
					  "fieldPostPublisher": "帖子發佈者",
 | 
				
			||||||
  "fieldPostContent": "發生什麼事了?!",
 | 
					  "fieldPostContent": "發生什麼事了?!",
 | 
				
			||||||
  "fieldPostTitle": "標題",
 | 
					  "fieldPostTitle": "標題",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答獎勵源點",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "標籤",
 | 
					  "fieldPostTags": "標籤",
 | 
				
			||||||
  "postPublish": "釋出",
 | 
					  "fieldPostCategories": "分類",
 | 
				
			||||||
  "postPublishedAt": "釋出於",
 | 
					  "fieldPostAlias": "別名",
 | 
				
			||||||
  "postPublishedUntil": "取消釋出於",
 | 
					  "fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
 | 
				
			||||||
 | 
					  "postPublish": "發佈",
 | 
				
			||||||
 | 
					  "postPublishedAt": "發佈於",
 | 
				
			||||||
 | 
					  "postPublishedUntil": "取消發佈於",
 | 
				
			||||||
  "postVisibility": "可見性",
 | 
					  "postVisibility": "可見性",
 | 
				
			||||||
  "postVisibilityDescription": "帖子可見性決定了誰能檢視該篇帖子。",
 | 
					  "postVisibilityDescription": "帖子可見性決定了誰能查看該篇帖子。",
 | 
				
			||||||
  "postVisibilityAll": "所有人可見",
 | 
					  "postVisibilityAll": "所有人可見",
 | 
				
			||||||
  "postVisibilityFriends": "僅限好友可見",
 | 
					  "postVisibilityFriends": "僅限好友可見",
 | 
				
			||||||
  "postVisibilitySelected": "選定的使用者可見",
 | 
					  "postVisibilitySelected": "選定的用戶可見",
 | 
				
			||||||
  "postVisibilityFiltered": "選定使用者不可見",
 | 
					  "postVisibilityFiltered": "選定用戶不可見",
 | 
				
			||||||
  "postVisibilityNone": "僅自己可見",
 | 
					  "postVisibilityNone": "僅自己可見",
 | 
				
			||||||
  "postVisibleUsers": "可見的使用者",
 | 
					  "postVisibleUsers": "可見的用戶",
 | 
				
			||||||
  "postInvisibleUsers": "不可見的使用者",
 | 
					  "postInvisibleUsers": "不可見的用戶",
 | 
				
			||||||
  "postSelectedUsers": {
 | 
					  "postSelectedUsers": {
 | 
				
			||||||
    "zero": "未選擇使用者",
 | 
					    "zero": "未選擇用戶",
 | 
				
			||||||
    "one": "選擇了 {} 個使用者",
 | 
					    "one": "選擇了 {} 個用戶",
 | 
				
			||||||
    "other": "選擇了 {} 個使用者"
 | 
					    "other": "選擇了 {} 個用戶"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "postEditingNotice": "你正在修改由 {} 釋出的帖子。",
 | 
					  "postEditingNotice": "你正在修改由 {} 發佈的帖子。",
 | 
				
			||||||
  "postReplyingNotice": "你正在回覆由 {} 釋出的帖子。",
 | 
					  "postReplyingNotice": "你正在回覆由 {} 發佈的帖子。",
 | 
				
			||||||
  "postRepostingNotice": "你正在轉發由 {} 釋出的帖子。",
 | 
					  "postRepostingNotice": "你正在轉發由 {} 發佈的帖子。",
 | 
				
			||||||
  "postReact": "反應",
 | 
					  "postReact": "反應",
 | 
				
			||||||
  "postPosted": "帖子已經發表。",
 | 
					  "postPosted": "帖子已經發表。",
 | 
				
			||||||
  "postReactions": "帖子的反應",
 | 
					  "postReactions": "帖子的反應",
 | 
				
			||||||
@@ -161,7 +188,7 @@
 | 
				
			|||||||
    "one": "{} 點社會信用點變更",
 | 
					    "one": "{} 點社會信用點變更",
 | 
				
			||||||
    "other": "{} 點社會信用點變更"
 | 
					    "other": "{} 點社會信用點變更"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "postReactCompleted": "反應已被新增。",
 | 
					  "postReactCompleted": "反應已被添加。",
 | 
				
			||||||
  "postReactUncompleted": "反應已被移除。",
 | 
					  "postReactUncompleted": "反應已被移除。",
 | 
				
			||||||
  "postComments": {
 | 
					  "postComments": {
 | 
				
			||||||
    "zero": "評論",
 | 
					    "zero": "評論",
 | 
				
			||||||
@@ -174,64 +201,92 @@
 | 
				
			|||||||
    "other": "{} 條評論"
 | 
					    "other": "{} 條評論"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "外觀",
 | 
					  "settingsAppearance": "外觀",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "顯示語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "跟隨系統",
 | 
				
			||||||
  "settingsBackgroundImage": "背景圖片",
 | 
					  "settingsBackgroundImage": "背景圖片",
 | 
				
			||||||
  "settingsBackgroundImageDescription": "設定應用全域性生效的的背景圖片。",
 | 
					  "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
 | 
				
			||||||
  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
					  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
				
			||||||
  "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
 | 
					  "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
 | 
				
			||||||
  "settingsThemeMaterial3": "使用 Material You 設計正規化",
 | 
					  "settingsThemeMaterial3": "使用 Material You 設計範式",
 | 
				
			||||||
  "settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。",
 | 
					  "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
 | 
				
			||||||
  "settingsNetwork": "網路",
 | 
					  "settingsAppBarTransparent": "透明頂欄",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 伺服器",
 | 
					  "settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
 | 
				
			||||||
  "settingsNetworkServerDescription": "設定 HyperNet 伺服器地址,選擇我們提供的,或者自己搭建。",
 | 
					  "settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
 | 
				
			||||||
  "settingsNetworkServerReset": "重設為官方伺服器",
 | 
					  "settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
 | 
				
			||||||
  "settingsNetworkServerResetDescription": "重設為 Solar Network 的伺服器地址。",
 | 
					  "settingsColorScheme": "主題色",
 | 
				
			||||||
  "settingsNetworkServerPreset": "預設的 HyperNet 伺服器",
 | 
					  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
				
			||||||
  "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 伺服器地址。",
 | 
					  "settingsColorSeed": "預設色彩主題",
 | 
				
			||||||
  "settingsNetworkServerSaved": "伺服器地址已儲存。",
 | 
					  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
				
			||||||
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHaptic": "新通知時振動",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展開帖子鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展開聊天鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
				
			||||||
 | 
					  "settingsNetwork": "網絡",
 | 
				
			||||||
 | 
					  "settingsNetworkServer": "HyperNet 服務器",
 | 
				
			||||||
 | 
					  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
				
			||||||
 | 
					  "settingsNetworkServerReset": "重設為官方服務器",
 | 
				
			||||||
 | 
					  "settingsNetworkServerResetDescription": "重設為 Solar Network 的服務器地址。",
 | 
				
			||||||
 | 
					  "settingsNetworkServerPreset": "預設的 HyperNet 服務器",
 | 
				
			||||||
 | 
					  "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 服務器地址。",
 | 
				
			||||||
 | 
					  "settingsNetworkServerSaved": "服務器地址已保存。",
 | 
				
			||||||
 | 
					  "settingsPerformance": "性能",
 | 
				
			||||||
 | 
					  "settingsImageQuality": "圖片預覽質量",
 | 
				
			||||||
 | 
					  "settingsImageQualityDescription": "設置圖片預覽質量,會影響圖片解碼速度。",
 | 
				
			||||||
 | 
					  "settingsImageQualityLowest": "極低",
 | 
				
			||||||
 | 
					  "settingsImageQualityLow": "低",
 | 
				
			||||||
 | 
					  "settingsImageQualityMedium": "中",
 | 
				
			||||||
 | 
					  "settingsImageQualityHigh": "高",
 | 
				
			||||||
  "settingsMisc": "雜項",
 | 
					  "settingsMisc": "雜項",
 | 
				
			||||||
  "settingsMiscAbout": "關於",
 | 
					  "settingsMiscAbout": "關於",
 | 
				
			||||||
  "settingsMiscAboutDescription": "檢視 Solian 的版本資訊。",
 | 
					  "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
 | 
				
			||||||
 | 
					  "settingsAccountLanguage": "帳號偏好語言",
 | 
				
			||||||
 | 
					  "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
 | 
				
			||||||
  "sensitiveContent": "敏感內容",
 | 
					  "sensitiveContent": "敏感內容",
 | 
				
			||||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
					  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
				
			||||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人檢視。",
 | 
					  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
				
			||||||
  "sensitiveContentReveal": "顯示內容",
 | 
					  "sensitiveContentReveal": "顯示內容",
 | 
				
			||||||
  "serverConnecting": "正在連線伺服器…",
 | 
					  "serverConnecting": "正在連接…",
 | 
				
			||||||
  "serverDisconnected": "已與伺服器斷開連線",
 | 
					  "serverDisconnected": "已斷開連接",
 | 
				
			||||||
 | 
					  "serverConnected": "已連接",
 | 
				
			||||||
  "fieldChatAlias": "頻道別名",
 | 
					  "fieldChatAlias": "頻道別名",
 | 
				
			||||||
  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
					  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
				
			||||||
  "fieldChatName": "名稱",
 | 
					  "fieldChatName": "名稱",
 | 
				
			||||||
  "fieldChatDescription": "描述",
 | 
					  "fieldChatDescription": "描述",
 | 
				
			||||||
  "fieldChatBelongToRealm": "所屬領域",
 | 
					  "fieldChatBelongToRealm": "所屬領域",
 | 
				
			||||||
  "fieldChatBelongToRealmUnset": "未設定頻道所屬領域",
 | 
					  "fieldChatBelongToRealmUnset": "未設置頻道所屬領域",
 | 
				
			||||||
  "channelEditingNotice": "您正在編輯頻道 {}",
 | 
					  "channelEditingNotice": "您正在編輯頻道 {}",
 | 
				
			||||||
  "channelDeleted": "聊天頻道 {} 已被刪除",
 | 
					  "channelDeleted": "聊天頻道 {} 已被刪除",
 | 
				
			||||||
  "channelDelete": "刪除聊天頻道 {}",
 | 
					  "channelDelete": "刪除聊天頻道 {}",
 | 
				
			||||||
  "channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有訊息將被永久刪除。",
 | 
					  "channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有消息將被永久刪除。",
 | 
				
			||||||
  "channelDetailPersonalRegion": "個人區域",
 | 
					  "channelDetailPersonalRegion": "個人區域",
 | 
				
			||||||
  "channelDetailMemberRegion": "成員管理",
 | 
					  "channelDetailMemberRegion": "成員管理",
 | 
				
			||||||
  "channelMemberManage": "管理成員",
 | 
					  "channelMemberManage": "管理成員",
 | 
				
			||||||
  "channelMemberManageDescription": "管理頻道內現有成員。",
 | 
					  "channelMemberManageDescription": "管理頻道內現有成員。",
 | 
				
			||||||
  "channelMemberAdd": "新增成員",
 | 
					  "channelMemberAdd": "添加成員",
 | 
				
			||||||
  "channelMemberAddDescription": "給當前頻道新增新成員。",
 | 
					  "channelMemberAddDescription": "給當前頻道添加新成員。",
 | 
				
			||||||
  "channelMemberAdded": "頻道成員已新增。",
 | 
					  "channelMemberAdded": "頻道成員已添加。",
 | 
				
			||||||
  "fieldMemberRelatedName": "成員名 / 賬戶 ID",
 | 
					  "fieldMemberRelatedName": "成員名 / 賬戶 ID",
 | 
				
			||||||
  "channelDetailAdminRegion": "管理區域",
 | 
					  "channelDetailAdminRegion": "管理區域",
 | 
				
			||||||
  "channelEditProfile": "更改頻道身份",
 | 
					  "channelEditProfile": "更改頻道身份",
 | 
				
			||||||
  "channelEdit": "編輯頻道",
 | 
					  "channelEdit": "編輯頻道",
 | 
				
			||||||
  "channelEditDescription": "更改頻道基本資訊,元資料等。",
 | 
					  "channelEditDescription": "更改頻道基本信息,元數據等。",
 | 
				
			||||||
  "channelProfileEdit": "編輯頻道身份",
 | 
					  "channelProfileEdit": "編輯頻道身份",
 | 
				
			||||||
  "channelActionDelete": "刪除頻道",
 | 
					  "channelActionDelete": "刪除頻道",
 | 
				
			||||||
  "channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有資訊。",
 | 
					  "channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有信息。",
 | 
				
			||||||
  "channelLeave": "退出頻道 {}",
 | 
					  "channelLeave": "退出頻道 {}",
 | 
				
			||||||
  "channelLeaveDescription": "退出該頻道,但是你頻道內的資訊不會被移除。",
 | 
					  "channelLeaveDescription": "退出該頻道,但是你頻道內的信息不會被移除。",
 | 
				
			||||||
  "channelActionLeave": "退出頻道",
 | 
					  "channelActionLeave": "退出頻道",
 | 
				
			||||||
  "channelActionLeaveDescription": "刪除你在這個頻道的身份。",
 | 
					  "channelActionLeaveDescription": "刪除你在這個頻道的身份。",
 | 
				
			||||||
  "channelNotifyLevel": "通知級別",
 | 
					  "channelNotifyLevel": "通知級別",
 | 
				
			||||||
  "channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的訊息。",
 | 
					  "channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的消息。",
 | 
				
			||||||
  "channelNotifyLevelAll": "全部通知",
 | 
					  "channelNotifyLevelAll": "全部通知",
 | 
				
			||||||
  "channelNotifyLevelMentioned": "僅提及",
 | 
					  "channelNotifyLevelMentioned": "僅提及",
 | 
				
			||||||
  "channelNotifyLevelNone": "全部靜音",
 | 
					  "channelNotifyLevelNone": "全部靜音",
 | 
				
			||||||
  "channelNotifyLevelApplied": "已經儲存並應用頻道通知級別配置。",
 | 
					  "channelNotifyLevelApplied": "已經保存並應用頻道通知級別配置。",
 | 
				
			||||||
  "fieldChannelProfileNick": "頻道內顯示名",
 | 
					  "fieldChannelProfileNick": "頻道內顯示名",
 | 
				
			||||||
  "fieldChannelProfileNickHint": "在頻道內顯示的暱稱,留空則使用賬號顯示名。",
 | 
					  "fieldChannelProfileNickHint": "在頻道內顯示的暱稱,留空則使用賬號顯示名。",
 | 
				
			||||||
  "fieldRealmAlias": "領域別名",
 | 
					  "fieldRealmAlias": "領域別名",
 | 
				
			||||||
@@ -241,38 +296,72 @@
 | 
				
			|||||||
  "realmEditingNotice": "您正在編輯領域 {}",
 | 
					  "realmEditingNotice": "您正在編輯領域 {}",
 | 
				
			||||||
  "realmDeleted": "領域 {} 已被刪除",
 | 
					  "realmDeleted": "領域 {} 已被刪除",
 | 
				
			||||||
  "realmDelete": "刪除領域 {}",
 | 
					  "realmDelete": "刪除領域 {}",
 | 
				
			||||||
  "realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、釋出者、製品等)都將被永久刪除。三思而後行!",
 | 
					  "realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
 | 
				
			||||||
  "realmActionDelete": "刪除領域",
 | 
					  "realmActionDelete": "刪除領域",
 | 
				
			||||||
  "realmActionDeleteDescription": "刪除整個領域及其附屬的資源。",
 | 
					  "realmActionDeleteDescription": "刪除整個領域及其附屬的資源。",
 | 
				
			||||||
  "realmEdit": "編輯領域",
 | 
					  "realmEdit": "編輯領域",
 | 
				
			||||||
  "realmEditDescription": "更改領域基本資訊,元資料等。",
 | 
					  "realmEditDescription": "更改領域基本信息,元數據等。",
 | 
				
			||||||
  "realmMemberAdd": "新增成員",
 | 
					  "realmMemberAdd": "添加成員",
 | 
				
			||||||
  "realmMemberAddDescription": "給當前領域新增新成員。",
 | 
					  "realmMemberAddDescription": "給當前領域添加新成員。",
 | 
				
			||||||
  "realmMemberAdded": "領域成員已新增。",
 | 
					  "realmMemberAdded": "領域成員已添加。",
 | 
				
			||||||
  "fieldChatMessage": "在 {} 中發訊息",
 | 
					  "fieldChatMessage": "在 {} 中發消息",
 | 
				
			||||||
  "fieldChatMessageDirect": "給 {} 發訊息",
 | 
					  "fieldChatMessageDirect": "給 {} 發消息",
 | 
				
			||||||
  "eventResourceTag": "訊息 {}",
 | 
					  "eventResourceTag": "消息 {}",
 | 
				
			||||||
  "messageDelete": "刪除訊息 {}",
 | 
					  "messageDelete": "刪除消息 {}",
 | 
				
			||||||
  "messageDeleteDescription": "你確定要刪除這個訊息嗎?該操作不可撤銷。同時您將留下一條刪除訊息的記錄。",
 | 
					  "messageDeleteDescription": "你確定要刪除這個消息嗎?該操作不可撤銷。同時您將留下一條刪除消息的記錄。",
 | 
				
			||||||
  "messageDeleted": "訊息 {} 已被刪除",
 | 
					  "messageDeleted": "消息 {} 已被刪除",
 | 
				
			||||||
  "messageEdited": "訊息 {} 已被編輯",
 | 
					  "messageEdited": "消息 {} 已被編輯",
 | 
				
			||||||
  "messageEditedHint": "已編輯",
 | 
					  "messageEditedHint": "已編輯",
 | 
				
			||||||
  "messageUnsupported": "不支援的訊息 {}",
 | 
					  "messageUnsupported": "不支持的消息 {}",
 | 
				
			||||||
  "messageFileHint": {
 | 
					  "messageFileHint": {
 | 
				
			||||||
    "zero": "沒有附件",
 | 
					    "zero": "沒有附件",
 | 
				
			||||||
    "one": "{} 個附件",
 | 
					    "one": "{} 個附件",
 | 
				
			||||||
    "other": "{} 個附件"
 | 
					    "other": "{} 個附件"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "addAttachmentFromAlbum": "從相簿中新增附件",
 | 
					  "messageTyping": {
 | 
				
			||||||
  "addAttachmentFromClipboard": "貼上附件",
 | 
					    "one": "{} 正在輸入",
 | 
				
			||||||
 | 
					    "other": "{} 正在輸入"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "fieldAttachmentRandomId": "訪問 ID",
 | 
				
			||||||
 | 
					  "fieldAttachmentAlt": "概述文字",
 | 
				
			||||||
 | 
					  "addAttachmentFromAlbum": "從相冊中添加附件",
 | 
				
			||||||
 | 
					  "addAttachmentFromClipboard": "粘貼附件",
 | 
				
			||||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
					  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "拍攝影片",
 | 
					  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
				
			||||||
  "attachmentPastedImage": "貼上的圖片",
 | 
					  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
				
			||||||
  "attachmentInsertLink": "插入連線",
 | 
					  "attachmentDetailInfo": "附件詳細信息",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "設定為帖子縮圖",
 | 
					  "attachmentPastedImage": "粘貼的圖片",
 | 
				
			||||||
  "attachmentUnsetAsPostThumbnail": "取消設定為帖子縮圖",
 | 
					  "attachmentInsertLink": "插入連接",
 | 
				
			||||||
  "attachmentSetThumbnail": "設定縮圖",
 | 
					  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
				
			||||||
 | 
					  "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
 | 
				
			||||||
 | 
					  "attachmentCompressVideo": "重新編碼視頻",
 | 
				
			||||||
 | 
					  "attachmentSetThumbnail": "設置縮略圖",
 | 
				
			||||||
 | 
					  "attachmentSetAlt": "設置概述文字",
 | 
				
			||||||
 | 
					  "attachmentCopyRandomId": "複製訪問 ID",
 | 
				
			||||||
  "attachmentUpload": "上傳",
 | 
					  "attachmentUpload": "上傳",
 | 
				
			||||||
 | 
					  "attachmentInputDialog": "上傳附件",
 | 
				
			||||||
 | 
					  "attachmentInputUseRandomId": "使用訪問 ID",
 | 
				
			||||||
 | 
					  "attachmentInputNew": "新上傳附件",
 | 
				
			||||||
 | 
					  "waitingForUpload": "等待上傳",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHint": "壓縮一份視頻的副本",
 | 
				
			||||||
 | 
					  "attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
 | 
				
			||||||
 | 
					  "attachmentCompressQuality": "壓縮質量",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHighest": "最高",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityDefault": "默認",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityMedium": "中等",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityLow": "低",
 | 
				
			||||||
 | 
					  "attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
 | 
				
			||||||
 | 
					  "attachmentUploaded": "已上傳",
 | 
				
			||||||
 | 
					  "attachmentPending": "未上傳",
 | 
				
			||||||
 | 
					  "attachmentCopyCompressed": "有壓縮副本",
 | 
				
			||||||
 | 
					  "attachmentGotBoosted": "有加速傳遞",
 | 
				
			||||||
 | 
					  "attachmentBoost": "加速包",
 | 
				
			||||||
 | 
					  "attachmentCreateBoost": "加速傳遞",
 | 
				
			||||||
 | 
					  "attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegion": "目標節點",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionAPAC": "亞太地區",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
 | 
				
			||||||
 | 
					  "attachmentDestinationRegionHKG": "香港",
 | 
				
			||||||
  "notification": "通知",
 | 
					  "notification": "通知",
 | 
				
			||||||
  "notificationUnreadCount": {
 | 
					  "notificationUnreadCount": {
 | 
				
			||||||
    "zero": "無未讀通知",
 | 
					    "zero": "無未讀通知",
 | 
				
			||||||
@@ -282,18 +371,18 @@
 | 
				
			|||||||
  "notificationUnread": "未讀",
 | 
					  "notificationUnread": "未讀",
 | 
				
			||||||
  "notificationRead": "已讀",
 | 
					  "notificationRead": "已讀",
 | 
				
			||||||
  "notificationMarkAllRead": "已讀所有通知",
 | 
					  "notificationMarkAllRead": "已讀所有通知",
 | 
				
			||||||
  "notificationMarkAllReadDescription": "您確定要將所有通知設定為已讀嗎?該操作不可撤銷。",
 | 
					  "notificationMarkAllReadDescription": "您確定要將所有通知設置為已讀嗎?該操作不可撤銷。",
 | 
				
			||||||
  "notificationMarkAllReadPrompt": {
 | 
					  "notificationMarkAllReadPrompt": {
 | 
				
			||||||
    "zero": "已將 0 個通知標記為已讀。",
 | 
					    "zero": "已將 0 個通知標記為已讀。",
 | 
				
			||||||
    "one": "已將 {} 個通知標記為已讀。",
 | 
					    "one": "已將 {} 個通知標記為已讀。",
 | 
				
			||||||
    "other": "已將 {} 個通知標記為已讀。"
 | 
					    "other": "已將 {} 個通知標記為已讀。"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "notificationMarkOneReadPrompt": "已將通知 {} 標記為已讀。",
 | 
					  "notificationMarkOneReadPrompt": "已將通知 {} 標記為已讀。",
 | 
				
			||||||
  "search": "搜尋",
 | 
					  "search": "搜索",
 | 
				
			||||||
  "postSearchResult": {
 | 
					  "postSearchResult": {
 | 
				
			||||||
    "zero": "沒有搜尋到結果",
 | 
					    "zero": "沒有搜索到結果",
 | 
				
			||||||
    "one": "搜尋到 {} 個結果",
 | 
					    "one": "搜索到 {} 個結果",
 | 
				
			||||||
    "other": "搜尋到 {} 個結果"
 | 
					    "other": "搜索到 {} 個結果"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "postSearchTook": "耗時 {}",
 | 
					  "postSearchTook": "耗時 {}",
 | 
				
			||||||
  "postDelete": "刪除帖子 {}",
 | 
					  "postDelete": "刪除帖子 {}",
 | 
				
			||||||
@@ -305,26 +394,26 @@
 | 
				
			|||||||
  "callResume": "恢復",
 | 
					  "callResume": "恢復",
 | 
				
			||||||
  "callMicrophone": "麥克風",
 | 
					  "callMicrophone": "麥克風",
 | 
				
			||||||
  "callCamera": "攝像頭",
 | 
					  "callCamera": "攝像頭",
 | 
				
			||||||
  "callMicrophoneDisabled": "麥克風已停用",
 | 
					  "callMicrophoneDisabled": "麥克風已禁用",
 | 
				
			||||||
  "callMicrophoneSelect": "選擇麥克風",
 | 
					  "callMicrophoneSelect": "選擇麥克風",
 | 
				
			||||||
  "callCameraDisabled": "攝像頭已停用",
 | 
					  "callCameraDisabled": "攝像頭已禁用",
 | 
				
			||||||
  "callCameraSelect": "選擇攝像頭",
 | 
					  "callCameraSelect": "選擇攝像頭",
 | 
				
			||||||
  "callDisconnected": "通話已斷開",
 | 
					  "callDisconnected": "通話已斷開",
 | 
				
			||||||
  "callEnded": "通話已結束",
 | 
					  "callEnded": "通話已結束",
 | 
				
			||||||
  "callStatusConnected": "已連線",
 | 
					  "callStatusConnected": "已連接",
 | 
				
			||||||
  "callStatusDisconnected": "未連線",
 | 
					  "callStatusDisconnected": "未連接",
 | 
				
			||||||
  "callStatusConnecting": "正在連線",
 | 
					  "callStatusConnecting": "正在連接",
 | 
				
			||||||
  "callStatusReconnecting": "正在重連",
 | 
					  "callStatusReconnecting": "正在重連",
 | 
				
			||||||
  "callDisconnect": "斷開連線",
 | 
					  "callDisconnect": "斷開連接",
 | 
				
			||||||
  "callDisconnectDescription": "您確定要與通話斷開連線嗎?",
 | 
					  "callDisconnectDescription": "您確定要與通話斷開連接嗎?",
 | 
				
			||||||
  "callMicrophoneOff": "關閉麥克風",
 | 
					  "callMicrophoneOff": "關閉麥克風",
 | 
				
			||||||
  "callMicrophoneOn": "開啟麥克風",
 | 
					  "callMicrophoneOn": "打開麥克風",
 | 
				
			||||||
  "callCameraOff": "關閉攝像頭",
 | 
					  "callCameraOff": "關閉攝像頭",
 | 
				
			||||||
  "callCameraOn": "開啟攝像頭",
 | 
					  "callCameraOn": "打開攝像頭",
 | 
				
			||||||
  "callVideoFlip": "映象畫面",
 | 
					  "callVideoFlip": "鏡像畫面",
 | 
				
			||||||
  "callSpeakerphoneToggle": "切換揚聲器",
 | 
					  "callSpeakerphoneToggle": "切換揚聲器",
 | 
				
			||||||
  "callScreenOff": "關閉螢幕共享",
 | 
					  "callScreenOff": "關閉屏幕共享",
 | 
				
			||||||
  "callScreenOn": "開啟螢幕共享",
 | 
					  "callScreenOn": "開啟屏幕共享",
 | 
				
			||||||
  "callMessageEnded": "通話持續了 {}",
 | 
					  "callMessageEnded": "通話持續了 {}",
 | 
				
			||||||
  "callMessageStarted": "通話開始了",
 | 
					  "callMessageStarted": "通話開始了",
 | 
				
			||||||
  "dailyCheckIn": "每日簽到",
 | 
					  "dailyCheckIn": "每日簽到",
 | 
				
			||||||
@@ -360,28 +449,53 @@
 | 
				
			|||||||
  "dailyCheckNegativeHint5Description": "關鍵時刻斷網",
 | 
					  "dailyCheckNegativeHint5Description": "關鍵時刻斷網",
 | 
				
			||||||
  "dailyCheckNegativeHint6": "出門",
 | 
					  "dailyCheckNegativeHint6": "出門",
 | 
				
			||||||
  "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
 | 
					  "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
 | 
				
			||||||
  "happyBirthday": "生日快樂,{}!",
 | 
					  "celebrateBirthday": "生日快樂,{}!",
 | 
				
			||||||
  "friendNew": "新增好友",
 | 
					  "celebrateLunarNewYear": "春節快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateMidAutumn": "中秋節快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateDragonBoat": "端午節快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateMerryXmas": "聖誕快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateNewYear": "新年快樂,{}!",
 | 
				
			||||||
 | 
					  "celebrateValentineDay": "今天是情人節,{}!",
 | 
				
			||||||
 | 
					  "celebrateLaborDay": "今天是勞動節,{}。",
 | 
				
			||||||
 | 
					  "celebrateMotherDay": "今天是母親節,{}。",
 | 
				
			||||||
 | 
					  "celebrateChildrenDay": "今天是兒童節,{}!",
 | 
				
			||||||
 | 
					  "celebrateFatherDay": "今天是父親節,{}。",
 | 
				
			||||||
 | 
					  "celebrateHalloween": "快樂在聖誕節,{}!",
 | 
				
			||||||
 | 
					  "celebrateThanksgiving": "今天是感恩節,{}!",
 | 
				
			||||||
 | 
					  "pendingLunarNewYear": "{} 過春節",
 | 
				
			||||||
 | 
					  "pendingMidAutumn": "{} 過中秋節",
 | 
				
			||||||
 | 
					  "pendingDragonBoat": "{} 過端午節",
 | 
				
			||||||
 | 
					  "pendingBirthday": "{} 過生日",
 | 
				
			||||||
 | 
					  "pendingMerryXmas": "{} 過聖誕節",
 | 
				
			||||||
 | 
					  "pendingNewYear": "{} 跨年",
 | 
				
			||||||
 | 
					  "pendingValentineDay": "{} 過情人節",
 | 
				
			||||||
 | 
					  "pendingLaborDay": "{} 過勞動節",
 | 
				
			||||||
 | 
					  "pendingMotherDay": "{} 過母親節",
 | 
				
			||||||
 | 
					  "pendingChildrenDay": "{} 過兒童節",
 | 
				
			||||||
 | 
					  "pendingFatherDay": "{} 過父親節",
 | 
				
			||||||
 | 
					  "pendingHalloween": "{} 過聖誕節",
 | 
				
			||||||
 | 
					  "pendingThanksgiving": "{} 過感恩節",
 | 
				
			||||||
 | 
					  "friendNew": "添加好友",
 | 
				
			||||||
  "friendRequests": "好友請求",
 | 
					  "friendRequests": "好友請求",
 | 
				
			||||||
  "friendRequestsDescription": {
 | 
					  "friendRequestsDescription": {
 | 
				
			||||||
    "zero": "你沒有好友請求",
 | 
					    "zero": "你沒有好友請求",
 | 
				
			||||||
    "one": "你有 {} 個好友請求",
 | 
					    "one": "你有 {} 個好友請求",
 | 
				
			||||||
    "other": "你有 {} 個好友請求"
 | 
					    "other": "你有 {} 個好友請求"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "friendBlocklist": "遮蔽列表",
 | 
					  "friendBlocklist": "屏蔽列表",
 | 
				
			||||||
  "friendBlocklistDescription": {
 | 
					  "friendBlocklistDescription": {
 | 
				
			||||||
    "zero": "你沒有遮蔽任何人",
 | 
					    "zero": "你沒有屏蔽任何人",
 | 
				
			||||||
    "one": "你遮蔽了 {} 個使用者",
 | 
					    "one": "你屏蔽了 {} 個用戶",
 | 
				
			||||||
    "other": "你遮蔽了 {} 個使用者"
 | 
					    "other": "你屏蔽了 {} 個用戶"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "friendStatusPending": "待處理",
 | 
					  "friendStatusPending": "待處理",
 | 
				
			||||||
  "friendStatusWaiting": "等待中",
 | 
					  "friendStatusWaiting": "等待中",
 | 
				
			||||||
  "friendStatusActive": "正活躍",
 | 
					  "friendStatusActive": "正活躍",
 | 
				
			||||||
  "friendStatusBlocked": "已遮蔽",
 | 
					  "friendStatusBlocked": "已屏蔽",
 | 
				
			||||||
  "friendRequestSent": "好友請求已傳送。",
 | 
					  "friendRequestSent": "好友請求已發送。",
 | 
				
			||||||
  "fieldFriendRelatedName": "好友名 / 賬戶 ID",
 | 
					  "fieldFriendRelatedName": "好友名 / 賬戶 ID",
 | 
				
			||||||
  "friendBlock": "遮蔽",
 | 
					  "friendBlock": "屏蔽",
 | 
				
			||||||
  "friendUnblock": "解除遮蔽",
 | 
					  "friendUnblock": "解除屏蔽",
 | 
				
			||||||
  "friendDeleteAction": "遺忘",
 | 
					  "friendDeleteAction": "遺忘",
 | 
				
			||||||
  "friendDelete": "遺忘跟 {} 的關係",
 | 
					  "friendDelete": "遺忘跟 {} 的關係",
 | 
				
			||||||
  "friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
 | 
					  "friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
 | 
				
			||||||
@@ -394,23 +508,25 @@
 | 
				
			|||||||
  "accountJoinedAt": "加入於 {}",
 | 
					  "accountJoinedAt": "加入於 {}",
 | 
				
			||||||
  "accountBirthday": "出生於 {}",
 | 
					  "accountBirthday": "出生於 {}",
 | 
				
			||||||
  "accountBadge": "徽章",
 | 
					  "accountBadge": "徽章",
 | 
				
			||||||
 | 
					  "accountCheckInNoRecords": "暫無運勢記錄",
 | 
				
			||||||
  "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
 | 
					  "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
 | 
				
			||||||
  "badgeSiteMigration": "Solar Network 原住民",
 | 
					  "badgeSiteMigration": "Solar Network 原住民",
 | 
				
			||||||
  "accountStatus": "狀態",
 | 
					  "accountStatus": "狀態",
 | 
				
			||||||
  "accountStatusOnline": "線上",
 | 
					  "accountStatusOnline": "在線",
 | 
				
			||||||
  "accountStatusOffline": "離線",
 | 
					  "accountStatusOffline": "離線",
 | 
				
			||||||
  "accountStatusLastSeen": "最後一次在 {} 上線",
 | 
					  "accountStatusLastSeen": "最後一次上線於 {}",
 | 
				
			||||||
  "postArticle": "Solar Network 上的文章",
 | 
					  "postArticle": "Solar Network 上的文章",
 | 
				
			||||||
  "postStory": "Solar Network 上的故事",
 | 
					  "postStory": "Solar Network 上的故事",
 | 
				
			||||||
 | 
					  "postLocalDraftRestored": "從本地恢復草稿",
 | 
				
			||||||
  "articleWrittenAt": "發表於 {}",
 | 
					  "articleWrittenAt": "發表於 {}",
 | 
				
			||||||
  "articleEditedAt": "編輯於 {}",
 | 
					  "articleEditedAt": "編輯於 {}",
 | 
				
			||||||
  "attachmentSaved": "已儲存到相簿",
 | 
					  "attachmentSaved": "已保存到相冊",
 | 
				
			||||||
  "attachmentSavedDesktop": "已儲存到下載目錄",
 | 
					  "attachmentSavedDesktop": "已保存到下載目錄",
 | 
				
			||||||
  "openInAlbum": "在相簿中開啟",
 | 
					  "openInAlbum": "在相冊中打開",
 | 
				
			||||||
  "postAbuseReport": "檢舉帖子",
 | 
					  "postAbuseReport": "檢舉帖子",
 | 
				
			||||||
  "postAbuseReportDescription": "檢舉不符合我們使用者協議以及社群準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感資訊。我們將會在 24 小時內處理您的檢舉。",
 | 
					  "postAbuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
 | 
				
			||||||
  "abuseReport": "檢舉",
 | 
					  "abuseReport": "檢舉",
 | 
				
			||||||
  "abuseReportDescription": "檢舉不符合我們使用者協議以及社群準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感資訊。我們將會在 24 小時內處理您的檢舉。",
 | 
					  "abuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
 | 
				
			||||||
  "abuseReportAction": "提交檢舉",
 | 
					  "abuseReportAction": "提交檢舉",
 | 
				
			||||||
  "abuseReportActionDescription": "檢舉不合規行為。",
 | 
					  "abuseReportActionDescription": "檢舉不合規行為。",
 | 
				
			||||||
  "abuseReportResource": "資源位置 / ID",
 | 
					  "abuseReportResource": "資源位置 / ID",
 | 
				
			||||||
@@ -418,28 +534,95 @@
 | 
				
			|||||||
  "abuseReportSubmitted": "檢舉已提交,感謝你的貢獻。",
 | 
					  "abuseReportSubmitted": "檢舉已提交,感謝你的貢獻。",
 | 
				
			||||||
  "submit": "提交",
 | 
					  "submit": "提交",
 | 
				
			||||||
  "accountDeletion": "刪除帳戶",
 | 
					  "accountDeletion": "刪除帳戶",
 | 
				
			||||||
  "accountDeletionDescription": "你確定要刪除這個帳戶嗎?該操作不可撤銷,其隸屬於該帳戶的所有資源(帖子、聊天頻道、釋出者、製品等)都將被永久刪除。三思而後行!",
 | 
					  "accountDeletionDescription": "你確定要刪除這個帳戶嗎?該操作不可撤銷,其隸屬於該帳戶的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
 | 
				
			||||||
  "accountDeletionActionDescription": "刪除你的 Solarpass 帳戶。",
 | 
					  "accountDeletionActionDescription": "刪除你的 Solarpass 帳戶。",
 | 
				
			||||||
  "accountDeletionSubmitted": "帳戶刪除申請已發出,你可以檢查你的收件箱並根據郵件內的指示完成刪除操作。",
 | 
					  "accountDeletionSubmitted": "帳戶刪除申請已發出,你可以檢查你的收件箱並根據郵件內的指示完成刪除操作。",
 | 
				
			||||||
  "channelNewChannel": "新建頻道",
 | 
					  "channelNewChannel": "新建頻道",
 | 
				
			||||||
  "channelNewDirectMessage": "發起私信",
 | 
					  "channelNewDirectMessage": "發起私信",
 | 
				
			||||||
  "channelDirectMessageDescription": "與 {} 的私聊",
 | 
					  "channelDirectMessageDescription": "與 {} 的私聊",
 | 
				
			||||||
  "fieldCannotBeEmpty": "此欄位不能為空。",
 | 
					  "fieldCannotBeEmpty": "此字段不能為空。",
 | 
				
			||||||
  "termAcceptLink": "瀏覽條款",
 | 
					  "termAcceptLink": "瀏覽條款",
 | 
				
			||||||
  "termAcceptNextWithAgree": "點選 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
 | 
					  "termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
 | 
				
			||||||
  "unauthorized": "未登陸",
 | 
					  "unauthorized": "未登陸",
 | 
				
			||||||
  "unauthorizedDescription": "登陸以探索整個 Solar Network。",
 | 
					  "unauthorizedDescription": "登陸以探索整個 Solar Network。",
 | 
				
			||||||
  "serviceStatus": "服務狀態",
 | 
					  "serviceStatus": "服務狀態",
 | 
				
			||||||
  "termRelated": "相關條款",
 | 
					  "termRelated": "相關條款",
 | 
				
			||||||
  "appDetails": "應用程式詳情",
 | 
					  "appDetails": "應用程序詳情",
 | 
				
			||||||
  "postRecommendation": "推薦帖子",
 | 
					  "postRecommendation": "推薦帖子",
 | 
				
			||||||
  "publisherBlockHint": "遮蔽 {}",
 | 
					  "publisherBlockHint": "屏蔽 {}",
 | 
				
			||||||
  "publisherBlockHintDescription": "你正要遮蔽此釋出者的運營者,該操作也將遮蔽由同一使用者運營的釋出者。",
 | 
					  "publisherBlockHintDescription": "你正要屏蔽此發佈者的運營者,該操作也將屏蔽由同一用戶運營的發佈者。",
 | 
				
			||||||
  "userUnblocked": "已解除遮蔽使用者 {}",
 | 
					  "userUnblocked": "已解除屏蔽用戶 {}",
 | 
				
			||||||
  "userBlocked": "已遮蔽使用者 {}",
 | 
					  "userBlocked": "已屏蔽用戶 {}",
 | 
				
			||||||
  "postSharingViaPicture": "正在生成帖子截圖,請稍等片刻……",
 | 
					  "postSharingViaPicture": "正在生成帖子截圖,請稍等片刻……",
 | 
				
			||||||
  "postImageShareReadMore": "掃描右側 QRCode 檢視全文",
 | 
					  "postImageShareReadMore": "掃描右側 QRCode 查看全文",
 | 
				
			||||||
  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
					  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
				
			||||||
  "postShare": "分享",
 | 
					  "postShare": "分享",
 | 
				
			||||||
  "postShareImage": "分享帖圖"
 | 
					  "postShareImage": "分享帖圖",
 | 
				
			||||||
 | 
					  "postGetInsight": "獲取見解",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI 見解",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
 | 
				
			||||||
 | 
					  "appInitializing": "正在初始化",
 | 
				
			||||||
 | 
					  "poweredBy": "由 {} 提供支持",
 | 
				
			||||||
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
 | 
					  "shareIntentDescription": "您想對您分享的內容做些什麼?",
 | 
				
			||||||
 | 
					  "shareIntentPostStory": "發佈動態",
 | 
				
			||||||
 | 
					  "shareIntentSendChannel": "分享到聊天頻道",
 | 
				
			||||||
 | 
					  "updateAvailable": "檢測到更新可用",
 | 
				
			||||||
 | 
					  "updateOngoing": "正在更新,請稍後……",
 | 
				
			||||||
 | 
					  "custom": "自定義",
 | 
				
			||||||
 | 
					  "colorSchemeIndigo": "靛藍",
 | 
				
			||||||
 | 
					  "colorSchemeBlue": "藍色",
 | 
				
			||||||
 | 
					  "colorSchemeGreen": "綠色",
 | 
				
			||||||
 | 
					  "colorSchemeYellow": "黃色",
 | 
				
			||||||
 | 
					  "colorSchemeOrange": "橙色",
 | 
				
			||||||
 | 
					  "colorSchemeRed": "紅色",
 | 
				
			||||||
 | 
					  "colorSchemeWhite": "白色",
 | 
				
			||||||
 | 
					  "colorSchemeBlack": "黑色",
 | 
				
			||||||
 | 
					  "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
 | 
				
			||||||
 | 
					  "postFeaturedComment": "精選評論",
 | 
				
			||||||
 | 
					  "postCategoryTechnology": "技術",
 | 
				
			||||||
 | 
					  "postCategoryGaming": "遊戲",
 | 
				
			||||||
 | 
					  "postCategoryLife": "生活",
 | 
				
			||||||
 | 
					  "postCategoryArts": "藝術",
 | 
				
			||||||
 | 
					  "postCategorySports": "體育",
 | 
				
			||||||
 | 
					  "postCategoryMusic": "音樂",
 | 
				
			||||||
 | 
					  "postCategoryNews": "新聞",
 | 
				
			||||||
 | 
					  "postCategoryKnowledge": "知識",
 | 
				
			||||||
 | 
					  "postCategoryLiterature": "文學",
 | 
				
			||||||
 | 
					  "postCategoryFunny": "搞笑",
 | 
				
			||||||
 | 
					  "postCategoryUncategorized": "未分類",
 | 
				
			||||||
 | 
					  "newsAllSources": "所有新聞",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "切換",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "你正在閱讀原始文章",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
 | 
				
			||||||
 | 
					  "newsToday": "快訊",
 | 
				
			||||||
 | 
					  "totpPostSetup": "還有一件事",
 | 
				
			||||||
 | 
					  "totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
 | 
				
			||||||
 | 
					  "totpNeverShare": "永遠不要分享這個 QR Code",
 | 
				
			||||||
 | 
					  "needHelp": "需要幫助?",
 | 
				
			||||||
 | 
					  "needHelpLaunch": "查看我們的山羊維基!",
 | 
				
			||||||
 | 
					  "walletCreate": "創建錢包",
 | 
				
			||||||
 | 
					  "walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
 | 
				
			||||||
 | 
					  "walletCreatePassword": "在下方設置你的付款密碼",
 | 
				
			||||||
 | 
					  "walletCurrencyShort": "源點",
 | 
				
			||||||
 | 
					  "walletCurrency": {
 | 
				
			||||||
 | 
					    "one": "{} 源點",
 | 
				
			||||||
 | 
					    "other": "{} 源點"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "aiThinkingProcess": "AI 思考過程",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "帳號設置已應用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精選解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "選擇解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
 | 
				
			||||||
 | 
					  "postVideoUpload": "上傳視頻",
 | 
				
			||||||
 | 
					  "realmJoin": "加入領域",
 | 
				
			||||||
 | 
					  "realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
 | 
				
			||||||
 | 
					  "realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
 | 
				
			||||||
 | 
					  "realmJoined": "已加入領域 {}。",
 | 
				
			||||||
 | 
					  "join": "加入"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										155
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						@@ -1,7 +1,7 @@
 | 
				
			|||||||
PODS:
 | 
					PODS:
 | 
				
			||||||
 | 
					  - Alamofire (5.10.2)
 | 
				
			||||||
  - connectivity_plus (0.0.1):
 | 
					  - connectivity_plus (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					 | 
				
			||||||
  - croppy (0.0.1):
 | 
					  - croppy (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - device_info_plus (0.0.1):
 | 
					  - device_info_plus (0.0.1):
 | 
				
			||||||
@@ -42,58 +42,58 @@ PODS:
 | 
				
			|||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - file_saver (0.0.1):
 | 
					  - file_saver (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - Firebase/Analytics (11.4.0):
 | 
					  - Firebase/Analytics (11.7.0):
 | 
				
			||||||
    - Firebase/Core
 | 
					    - Firebase/Core
 | 
				
			||||||
  - Firebase/Core (11.4.0):
 | 
					  - Firebase/Core (11.7.0):
 | 
				
			||||||
    - Firebase/CoreOnly
 | 
					    - Firebase/CoreOnly
 | 
				
			||||||
    - FirebaseAnalytics (~> 11.4.0)
 | 
					    - FirebaseAnalytics (~> 11.7.0)
 | 
				
			||||||
  - Firebase/CoreOnly (11.4.0):
 | 
					  - Firebase/CoreOnly (11.7.0):
 | 
				
			||||||
    - FirebaseCore (= 11.4.0)
 | 
					    - FirebaseCore (~> 11.7.0)
 | 
				
			||||||
  - Firebase/Messaging (11.4.0):
 | 
					  - Firebase/Messaging (11.7.0):
 | 
				
			||||||
    - Firebase/CoreOnly
 | 
					    - Firebase/CoreOnly
 | 
				
			||||||
    - FirebaseMessaging (~> 11.4.0)
 | 
					    - FirebaseMessaging (~> 11.7.0)
 | 
				
			||||||
  - firebase_analytics (11.3.6):
 | 
					  - firebase_analytics (11.4.2):
 | 
				
			||||||
    - Firebase/Analytics (= 11.4.0)
 | 
					    - Firebase/Analytics (= 11.7.0)
 | 
				
			||||||
    - firebase_core
 | 
					    - firebase_core
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - firebase_core (3.8.1):
 | 
					  - firebase_core (3.11.0):
 | 
				
			||||||
    - Firebase/CoreOnly (= 11.4.0)
 | 
					    - Firebase/CoreOnly (= 11.7.0)
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - firebase_messaging (15.1.6):
 | 
					  - firebase_messaging (15.2.2):
 | 
				
			||||||
    - Firebase/Messaging (= 11.4.0)
 | 
					    - Firebase/Messaging (= 11.7.0)
 | 
				
			||||||
    - firebase_core
 | 
					    - firebase_core
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - FirebaseAnalytics (11.4.0):
 | 
					  - FirebaseAnalytics (11.7.0):
 | 
				
			||||||
    - FirebaseAnalytics/AdIdSupport (= 11.4.0)
 | 
					    - FirebaseAnalytics/AdIdSupport (= 11.7.0)
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.7.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - FirebaseAnalytics/AdIdSupport (11.4.0):
 | 
					  - FirebaseAnalytics/AdIdSupport (11.7.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.7.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleAppMeasurement (= 11.4.0)
 | 
					    - GoogleAppMeasurement (= 11.7.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - FirebaseCore (11.4.0):
 | 
					  - FirebaseCore (11.7.0):
 | 
				
			||||||
    - FirebaseCoreInternal (~> 11.0)
 | 
					    - FirebaseCoreInternal (~> 11.7.0)
 | 
				
			||||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
					    - GoogleUtilities/Environment (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Logger (~> 8.0)
 | 
					    - GoogleUtilities/Logger (~> 8.0)
 | 
				
			||||||
  - FirebaseCoreInternal (11.6.0):
 | 
					  - FirebaseCoreInternal (11.7.0):
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
  - FirebaseInstallations (11.4.0):
 | 
					  - FirebaseInstallations (11.7.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.7.0)
 | 
				
			||||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
					    - GoogleUtilities/Environment (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
					    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
				
			||||||
    - PromisesObjC (~> 2.4)
 | 
					    - PromisesObjC (~> 2.4)
 | 
				
			||||||
  - FirebaseMessaging (11.4.0):
 | 
					  - FirebaseMessaging (11.7.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.7.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleDataTransport (~> 10.0)
 | 
					    - GoogleDataTransport (~> 10.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
@@ -102,32 +102,41 @@ 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_inappwebview_ios (0.0.1):
 | 
				
			||||||
 | 
					    - Flutter
 | 
				
			||||||
 | 
					    - flutter_inappwebview_ios/Core (= 0.0.1)
 | 
				
			||||||
 | 
					    - OrderedSet (~> 6.0.3)
 | 
				
			||||||
 | 
					  - flutter_inappwebview_ios/Core (0.0.1):
 | 
				
			||||||
 | 
					    - Flutter
 | 
				
			||||||
 | 
					    - OrderedSet (~> 6.0.3)
 | 
				
			||||||
  - flutter_native_splash (2.4.3):
 | 
					  - flutter_native_splash (2.4.3):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
  - flutter_webrtc (0.12.2):
 | 
					  - flutter_webrtc (0.12.6):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
  - gal (1.0.0):
 | 
					  - gal (1.0.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - GoogleAppMeasurement (11.4.0):
 | 
					  - GoogleAppMeasurement (11.7.0):
 | 
				
			||||||
    - GoogleAppMeasurement/AdIdSupport (= 11.4.0)
 | 
					    - GoogleAppMeasurement/AdIdSupport (= 11.7.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - GoogleAppMeasurement/AdIdSupport (11.4.0):
 | 
					  - GoogleAppMeasurement/AdIdSupport (11.7.0):
 | 
				
			||||||
    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
 | 
					    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
 | 
					  - GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
@@ -167,7 +176,10 @@ PODS:
 | 
				
			|||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - image_picker_ios (0.0.1):
 | 
					  - image_picker_ios (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - livekit_client (2.3.2):
 | 
					  - in_app_review (2.0.0):
 | 
				
			||||||
 | 
					    - Flutter
 | 
				
			||||||
 | 
					  - Kingfisher (8.2.0)
 | 
				
			||||||
 | 
					  - livekit_client (2.3.6):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
@@ -182,6 +194,7 @@ PODS:
 | 
				
			|||||||
    - nanopb/encode (= 3.30910.0)
 | 
					    - nanopb/encode (= 3.30910.0)
 | 
				
			||||||
  - nanopb/decode (3.30910.0)
 | 
					  - nanopb/decode (3.30910.0)
 | 
				
			||||||
  - nanopb/encode (3.30910.0)
 | 
					  - nanopb/encode (3.30910.0)
 | 
				
			||||||
 | 
					  - OrderedSet (6.0.3)
 | 
				
			||||||
  - package_info_plus (0.4.5):
 | 
					  - package_info_plus (0.4.5):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - pasteboard (0.0.1):
 | 
					  - pasteboard (0.0.1):
 | 
				
			||||||
@@ -211,14 +224,19 @@ PODS:
 | 
				
			|||||||
  - SwiftyGif (5.4.5)
 | 
					  - SwiftyGif (5.4.5)
 | 
				
			||||||
  - url_launcher_ios (0.0.1):
 | 
					  - url_launcher_ios (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
 | 
					  - video_compress (0.3.0):
 | 
				
			||||||
 | 
					    - Flutter
 | 
				
			||||||
  - volume_controller (0.0.1):
 | 
					  - volume_controller (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - 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:
 | 
				
			||||||
  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
 | 
					  - Alamofire
 | 
				
			||||||
 | 
					  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
 | 
				
			||||||
  - 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`)
 | 
				
			||||||
  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
					  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
				
			||||||
@@ -227,12 +245,16 @@ 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_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/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`)
 | 
				
			||||||
@@ -247,11 +269,14 @@ DEPENDENCIES:
 | 
				
			|||||||
  - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
 | 
					  - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
 | 
				
			||||||
  - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
 | 
					  - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
 | 
				
			||||||
  - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
 | 
					  - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
 | 
				
			||||||
 | 
					  - video_compress (from `.symlinks/plugins/video_compress/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,7 +288,9 @@ SPEC REPOS:
 | 
				
			|||||||
    - GoogleAppMeasurement
 | 
					    - GoogleAppMeasurement
 | 
				
			||||||
    - GoogleDataTransport
 | 
					    - GoogleDataTransport
 | 
				
			||||||
    - GoogleUtilities
 | 
					    - GoogleUtilities
 | 
				
			||||||
 | 
					    - Kingfisher
 | 
				
			||||||
    - nanopb
 | 
					    - nanopb
 | 
				
			||||||
 | 
					    - OrderedSet
 | 
				
			||||||
    - PromisesObjC
 | 
					    - PromisesObjC
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
    - SDWebImage
 | 
					    - SDWebImage
 | 
				
			||||||
@@ -272,7 +299,7 @@ SPEC REPOS:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
EXTERNAL SOURCES:
 | 
					EXTERNAL SOURCES:
 | 
				
			||||||
  connectivity_plus:
 | 
					  connectivity_plus:
 | 
				
			||||||
    :path: ".symlinks/plugins/connectivity_plus/darwin"
 | 
					    :path: ".symlinks/plugins/connectivity_plus/ios"
 | 
				
			||||||
  croppy:
 | 
					  croppy:
 | 
				
			||||||
    :path: ".symlinks/plugins/croppy/ios"
 | 
					    :path: ".symlinks/plugins/croppy/ios"
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
@@ -289,6 +316,10 @@ 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_inappwebview_ios:
 | 
				
			||||||
 | 
					    :path: ".symlinks/plugins/flutter_inappwebview_ios/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 +332,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:
 | 
				
			||||||
@@ -329,43 +362,53 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: ".symlinks/plugins/sqflite_darwin/darwin"
 | 
					    :path: ".symlinks/plugins/sqflite_darwin/darwin"
 | 
				
			||||||
  url_launcher_ios:
 | 
					  url_launcher_ios:
 | 
				
			||||||
    :path: ".symlinks/plugins/url_launcher_ios/ios"
 | 
					    :path: ".symlinks/plugins/url_launcher_ios/ios"
 | 
				
			||||||
 | 
					  video_compress:
 | 
				
			||||||
 | 
					    :path: ".symlinks/plugins/video_compress/ios"
 | 
				
			||||||
  volume_controller:
 | 
					  volume_controller:
 | 
				
			||||||
    :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:
 | 
				
			||||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
					  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
				
			||||||
 | 
					  connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
 | 
				
			||||||
  croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
 | 
					  croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
 | 
				
			||||||
  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
					  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
				
			||||||
  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
					  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
				
			||||||
  DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
 | 
					  DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
 | 
				
			||||||
  file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
 | 
					  file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
 | 
				
			||||||
  file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
 | 
					  file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
 | 
				
			||||||
  Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
 | 
					  Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
 | 
				
			||||||
  firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
 | 
					  firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107
 | 
				
			||||||
  firebase_core: 418aed674e9a0b8b6088aec16cde82a811f6261f
 | 
					  firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4
 | 
				
			||||||
  firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
 | 
					  firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f
 | 
				
			||||||
  FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
 | 
					  FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
 | 
				
			||||||
  FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
 | 
					  FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
 | 
				
			||||||
  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
					  FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
 | 
				
			||||||
  FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
 | 
					  FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
 | 
				
			||||||
  FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
 | 
					  FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
 | 
				
			||||||
  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
					  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
				
			||||||
  flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
 | 
					  flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
 | 
				
			||||||
  flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
 | 
					  flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
 | 
				
			||||||
  flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
 | 
					  flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
 | 
				
			||||||
  gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
 | 
					  flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
 | 
				
			||||||
  GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
 | 
					  flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
 | 
				
			||||||
 | 
					  gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
 | 
				
			||||||
 | 
					  GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
 | 
				
			||||||
  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
					  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
				
			||||||
  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
					  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
				
			||||||
  home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
 | 
					  home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
 | 
				
			||||||
  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
					  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
				
			||||||
  livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd
 | 
					  in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
 | 
				
			||||||
 | 
					  Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
 | 
				
			||||||
 | 
					  livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
 | 
				
			||||||
  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
					  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
				
			||||||
  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
					  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
				
			||||||
  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
					  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
				
			||||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
					  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
				
			||||||
 | 
					  OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
 | 
				
			||||||
  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
 | 
					  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
 | 
				
			||||||
  pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
 | 
					  pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
 | 
				
			||||||
  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
					  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
				
			||||||
@@ -380,10 +423,12 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
 | 
					  sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
 | 
				
			||||||
  SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
 | 
					  SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
 | 
				
			||||||
  url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
 | 
					  url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
 | 
				
			||||||
 | 
					  video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
 | 
				
			||||||
  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 {
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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 avatarUrl = getAttachmentUrl(for: avatarIdentifier)
 | 
					        let replyableMessageCategory = UNNotificationCategory(
 | 
				
			||||||
        fetchAvatarImage(from: avatarUrl) { [weak self] inImage in
 | 
					            identifier: content.categoryIdentifier,
 | 
				
			||||||
            guard let self = self else { return }
 | 
					            actions: [
 | 
				
			||||||
 | 
					                UNTextInputNotificationAction(
 | 
				
			||||||
 | 
					                    identifier: "reply_action",
 | 
				
			||||||
 | 
					                    title: "Reply",
 | 
				
			||||||
 | 
					                    options: []
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            intentIdentifiers: [],
 | 
				
			||||||
 | 
					            options: []
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
            let handle = INPersonHandle(value: "\(metadata["user_id"] ?? "")", type: .unknown)
 | 
					        UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
 | 
				
			||||||
 | 
					        content.categoryIdentifier = replyableMessageCategory.identifier
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let metadataCopy = metadata as? [String: String] ?? [:]
 | 
				
			||||||
 | 
					        let avatarUrl = getAttachmentUrl(for: avatarIdentifier)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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: "\(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)
 | 
					 | 
				
			||||||
            session.downloadTask(with: remoteUrl) { [weak content] localUrl, response, error in
 | 
					 | 
				
			||||||
                guard let content = content else { return }
 | 
					 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
                if let error = error {
 | 
					            switch result {
 | 
				
			||||||
                    print("Failed to download media: \(error.localizedDescription)")
 | 
					            case .success(let retrievalResult):
 | 
				
			||||||
                    self.contentHandler?(content)
 | 
					                // The image is either retrieved from cache or downloaded
 | 
				
			||||||
                    return
 | 
					                let tempDirectory = FileManager.default.temporaryDirectory
 | 
				
			||||||
                }
 | 
					                let cachedFileUrl = tempDirectory.appendingPathComponent(identifier)
 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                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:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
import 'dart:math' as math;
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
@@ -11,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
 | 
				
			|||||||
import 'package:surface/providers/user_directory.dart';
 | 
					import 'package:surface/providers/user_directory.dart';
 | 
				
			||||||
import 'package:surface/providers/websocket.dart';
 | 
					import 'package:surface/providers/websocket.dart';
 | 
				
			||||||
import 'package:surface/types/chat.dart';
 | 
					import 'package:surface/types/chat.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/websocket.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChatMessageController extends ChangeNotifier {
 | 
					class ChatMessageController extends ChangeNotifier {
 | 
				
			||||||
@@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  int? messageTotal;
 | 
					  int? messageTotal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool get isAllLoaded =>
 | 
					  bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
 | 
				
			||||||
      messageTotal != null && messages.length >= messageTotal!;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? _boxKey;
 | 
					  String? _boxKey;
 | 
				
			||||||
  SnChannel? channel;
 | 
					  SnChannel? channel;
 | 
				
			||||||
@@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
  /// Stored as a list of nonce to provide the loading state
 | 
					  /// Stored as a list of nonce to provide the loading state
 | 
				
			||||||
  final List<String> unconfirmedMessages = List.empty(growable: true);
 | 
					  final List<String> unconfirmedMessages = List.empty(growable: true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Box<SnChatMessage>? get _box =>
 | 
					  Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
 | 
				
			||||||
      (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
 | 
					
 | 
				
			||||||
 | 
					  final List<SnChannelMember> typingMembers = List.empty(growable: true);
 | 
				
			||||||
 | 
					  final Map<int, Timer> typingInactiveTimer = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> initialize(SnChannel chan) async {
 | 
					  Future<void> initialize(SnChannel chan) async {
 | 
				
			||||||
    channel = chan;
 | 
					    channel = chan;
 | 
				
			||||||
@@ -68,9 +71,10 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
      resp.data as Map<String, dynamic>,
 | 
					      resp.data as Map<String, dynamic>,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _wsSubscription = _ws.stream.stream.listen((event) {
 | 
					    _wsSubscription = _ws.pk.stream.listen((event) {
 | 
				
			||||||
      switch (event.method) {
 | 
					      switch (event.method) {
 | 
				
			||||||
        case 'events.new':
 | 
					        case 'events.new':
 | 
				
			||||||
 | 
					          if (event.payload?['channel_id'] != channel?.id) break;
 | 
				
			||||||
          final payload = SnChatMessage.fromJson(event.payload!);
 | 
					          final payload = SnChatMessage.fromJson(event.payload!);
 | 
				
			||||||
          _addMessage(payload);
 | 
					          _addMessage(payload);
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
@@ -78,22 +82,16 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
          if (event.payload?['channel_id'] != channel?.id) break;
 | 
					          if (event.payload?['channel_id'] != channel?.id) break;
 | 
				
			||||||
          final member = SnChannelMember.fromJson(event.payload!['member']);
 | 
					          final member = SnChannelMember.fromJson(event.payload!['member']);
 | 
				
			||||||
          if (member.id == profile?.id) break;
 | 
					          if (member.id == profile?.id) break;
 | 
				
			||||||
        // TODO impl typing users
 | 
					          if (!typingMembers.any((x) => x.id == member.id)) {
 | 
				
			||||||
        // if (!_typingUsers.any((x) => x.id == member.id)) {
 | 
					            typingMembers.add(member);
 | 
				
			||||||
        //   setState(() {
 | 
					            notifyListeners();
 | 
				
			||||||
        //     _typingUsers.add(member);
 | 
					          }
 | 
				
			||||||
        //   });
 | 
					          typingInactiveTimer[member.id]?.cancel();
 | 
				
			||||||
        // }
 | 
					          typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
 | 
				
			||||||
        // _typingInactiveTimer[member.id]?.cancel();
 | 
					            typingMembers.removeWhere((x) => x.id == member.id);
 | 
				
			||||||
        // _typingInactiveTimer[member.id] = Timer(
 | 
					            typingInactiveTimer.remove(member.id);
 | 
				
			||||||
        //   const Duration(seconds: 3),
 | 
					            notifyListeners();
 | 
				
			||||||
        //   () {
 | 
					          });
 | 
				
			||||||
        //     setState(() {
 | 
					 | 
				
			||||||
        //       _typingUsers.removeWhere((x) => x.id == member.id);
 | 
					 | 
				
			||||||
        //       _typingInactiveTimer.remove(member.id);
 | 
					 | 
				
			||||||
        //     });
 | 
					 | 
				
			||||||
        //   },
 | 
					 | 
				
			||||||
        // );
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Timer? _typingNotifyTimer;
 | 
				
			||||||
 | 
					  bool _typingStatus = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _sendTypingStatusPackage() async {
 | 
				
			||||||
 | 
					    _ws.conn?.sink.add(jsonEncode(
 | 
				
			||||||
 | 
					      WebSocketPackage(
 | 
				
			||||||
 | 
					        method: 'status.typing',
 | 
				
			||||||
 | 
					        endpoint: 'im',
 | 
				
			||||||
 | 
					        payload: {
 | 
				
			||||||
 | 
					          'channel_id': channel!.id,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ).toJson(),
 | 
				
			||||||
 | 
					    ));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void pingTypingStatus() {
 | 
				
			||||||
 | 
					    if (!_typingStatus) {
 | 
				
			||||||
 | 
					      _sendTypingStatusPackage();
 | 
				
			||||||
 | 
					      _typingStatus = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
 | 
				
			||||||
 | 
					      _typingNotifyTimer?.cancel();
 | 
				
			||||||
 | 
					      _typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
 | 
				
			||||||
 | 
					        _typingStatus = false;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
 | 
					  Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
 | 
				
			||||||
    if (_box == null) return;
 | 
					    if (_box == null) return;
 | 
				
			||||||
    await _box!.putAll({
 | 
					    await _box!.putAll({
 | 
				
			||||||
@@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
    switch (message.type) {
 | 
					    switch (message.type) {
 | 
				
			||||||
      case 'messages.edit':
 | 
					      case 'messages.edit':
 | 
				
			||||||
        if (message.relatedEventId != null) {
 | 
					        if (message.relatedEventId != null) {
 | 
				
			||||||
          final idx =
 | 
					          final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
 | 
				
			||||||
              messages.indexWhere((x) => x.id == message.relatedEventId);
 | 
					 | 
				
			||||||
          if (idx != -1) {
 | 
					          if (idx != -1) {
 | 
				
			||||||
            final newBody = message.body;
 | 
					            final newBody = message.body;
 | 
				
			||||||
            newBody.remove('related_event');
 | 
					            newBody.remove('related_event');
 | 
				
			||||||
@@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
      'algorithm': 'plain',
 | 
					      'algorithm': 'plain',
 | 
				
			||||||
      if (quoteId != null) 'quote_event': quoteId,
 | 
					      if (quoteId != null) 'quote_event': quoteId,
 | 
				
			||||||
      if (relatedId != null) 'related_event': relatedId,
 | 
					      if (relatedId != null) 'related_event': relatedId,
 | 
				
			||||||
      if (attachments != null && attachments.isNotEmpty)
 | 
					      if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
 | 
				
			||||||
        'attachments': attachments,
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Mock the message locally
 | 
					    // Mock the message locally
 | 
				
			||||||
@@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (out == null) {
 | 
					    if (out == null) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        final resp = await _sn.client
 | 
					        final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
 | 
				
			||||||
            .get('/cgi/im/channels/${channel!.keyPath}/events/$id');
 | 
					 | 
				
			||||||
        out = SnChatMessage.fromJson(resp.data);
 | 
					        out = SnChatMessage.fromJson(resp.data);
 | 
				
			||||||
        _saveMessageToLocal([out]);
 | 
					        _saveMessageToLocal([out]);
 | 
				
			||||||
      } catch (_) {
 | 
					      } catch (_) {
 | 
				
			||||||
@@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
    bool forceRemote = false,
 | 
					    bool forceRemote = false,
 | 
				
			||||||
  }) async {
 | 
					  }) async {
 | 
				
			||||||
    late List<SnChatMessage> out;
 | 
					    late List<SnChatMessage> out;
 | 
				
			||||||
    if (_box != null &&
 | 
					    if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
 | 
				
			||||||
        (_box!.length >= take + offset || forceLocal) &&
 | 
					 | 
				
			||||||
        !forceRemote) {
 | 
					 | 
				
			||||||
      out = _box!.keys
 | 
					      out = _box!.keys
 | 
				
			||||||
          .toList()
 | 
					          .toList()
 | 
				
			||||||
          .cast<int>()
 | 
					          .cast<int>()
 | 
				
			||||||
@@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
          quoteEvent: quoteEvent,
 | 
					          quoteEvent: quoteEvent,
 | 
				
			||||||
          attachments: attachments
 | 
					          attachments: attachments
 | 
				
			||||||
              .where(
 | 
					              .where(
 | 
				
			||||||
                (ele) =>
 | 
					                (ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
 | 
				
			||||||
                    out[i].body['attachments']?.contains(ele?.rid) ?? false,
 | 
					 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
              .toList(),
 | 
					              .toList(),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Preload sender accounts
 | 
					    // Preload sender accounts
 | 
				
			||||||
    final accountId = out
 | 
					    final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
 | 
				
			||||||
        .where((ele) => ele.sender.accountId >= 0)
 | 
					 | 
				
			||||||
        .map((ele) => ele.sender.accountId)
 | 
					 | 
				
			||||||
        .toSet();
 | 
					 | 
				
			||||||
    await _ud.listAccount(accountId);
 | 
					    await _ud.listAccount(accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return out;
 | 
					    return out;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,29 +1,30 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					import 'dart:developer';
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.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:image_picker/image_picker.dart';
 | 
					import 'package:image_picker/image_picker.dart';
 | 
				
			||||||
import 'package:mime/mime.dart';
 | 
					import 'package:mime/mime.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:shared_preferences/shared_preferences.dart';
 | 
				
			||||||
import 'package:surface/providers/post.dart';
 | 
					import 'package:surface/providers/post.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/types/attachment.dart';
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/poll.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					import 'package:video_compress/video_compress.dart';
 | 
				
			||||||
enum PostWriteMediaType {
 | 
					 | 
				
			||||||
  image,
 | 
					 | 
				
			||||||
  video,
 | 
					 | 
				
			||||||
  audio,
 | 
					 | 
				
			||||||
  file,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostWriteMedia {
 | 
					class PostWriteMedia {
 | 
				
			||||||
  late String name;
 | 
					  late String name;
 | 
				
			||||||
  late PostWriteMediaType type;
 | 
					  late SnMediaType type;
 | 
				
			||||||
  final SnAttachment? attachment;
 | 
					  final SnAttachment? attachment;
 | 
				
			||||||
  final XFile? file;
 | 
					  final XFile? file;
 | 
				
			||||||
  final Uint8List? raw;
 | 
					  final Uint8List? raw;
 | 
				
			||||||
@@ -35,16 +36,16 @@ class PostWriteMedia {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    switch (attachment?.mimetype.split('/').firstOrNull) {
 | 
					    switch (attachment?.mimetype.split('/').firstOrNull) {
 | 
				
			||||||
      case 'image':
 | 
					      case 'image':
 | 
				
			||||||
        type = PostWriteMediaType.image;
 | 
					        type = SnMediaType.image;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case 'video':
 | 
					      case 'video':
 | 
				
			||||||
        type = PostWriteMediaType.video;
 | 
					        type = SnMediaType.video;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case 'audio':
 | 
					      case 'audio':
 | 
				
			||||||
        type = PostWriteMediaType.audio;
 | 
					        type = SnMediaType.audio;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        type = PostWriteMediaType.file;
 | 
					        type = SnMediaType.file;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,16 +57,16 @@ class PostWriteMedia {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    switch (mimetype?.split('/').firstOrNull) {
 | 
					    switch (mimetype?.split('/').firstOrNull) {
 | 
				
			||||||
      case 'image':
 | 
					      case 'image':
 | 
				
			||||||
        type = PostWriteMediaType.image;
 | 
					        type = SnMediaType.image;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case 'video':
 | 
					      case 'video':
 | 
				
			||||||
        type = PostWriteMediaType.video;
 | 
					        type = SnMediaType.video;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case 'audio':
 | 
					      case 'audio':
 | 
				
			||||||
        type = PostWriteMediaType.audio;
 | 
					        type = SnMediaType.audio;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        type = PostWriteMediaType.file;
 | 
					        type = SnMediaType.file;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -104,7 +105,7 @@ class PostWriteMedia {
 | 
				
			|||||||
    if (attachment != null) {
 | 
					    if (attachment != null) {
 | 
				
			||||||
      final sn = context.read<SnNetworkProvider>();
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
      final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
 | 
					      final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
 | 
				
			||||||
      if (width != null && height != null) {
 | 
					      if (width != null && height != null && !kIsWeb) {
 | 
				
			||||||
        return ResizeImage(
 | 
					        return ResizeImage(
 | 
				
			||||||
          provider,
 | 
					          provider,
 | 
				
			||||||
          width: width,
 | 
					          width: width,
 | 
				
			||||||
@@ -144,6 +145,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
  static const Map<String, String> kTitleMap = {
 | 
					  static const Map<String, String> kTitleMap = {
 | 
				
			||||||
    'stories': 'writePostTypeStory',
 | 
					    'stories': 'writePostTypeStory',
 | 
				
			||||||
    'articles': 'writePostTypeArticle',
 | 
					    'articles': 'writePostTypeArticle',
 | 
				
			||||||
 | 
					    'questions': 'writePostTypeQuestion',
 | 
				
			||||||
 | 
					    'videos': 'writePostTypeVideo',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const kAttachmentProgressWeight = 0.9;
 | 
					  static const kAttachmentProgressWeight = 0.9;
 | 
				
			||||||
@@ -152,10 +155,26 @@ 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();
 | 
				
			||||||
 | 
					  final TextEditingController rewardController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  PostWriteController() {
 | 
					  bool _temporarySaveActive = false;
 | 
				
			||||||
    titleController.addListener(() => notifyListeners());
 | 
					
 | 
				
			||||||
    descriptionController.addListener(() => notifyListeners());
 | 
					  PostWriteController({bool doLoadFromTemporary = true}) {
 | 
				
			||||||
 | 
					    _temporarySaveActive = doLoadFromTemporary;
 | 
				
			||||||
 | 
					    titleController.addListener(() {
 | 
				
			||||||
 | 
					      _temporaryPlanSave();
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    descriptionController.addListener(() {
 | 
				
			||||||
 | 
					      _temporaryPlanSave();
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    contentController.addListener(() {
 | 
				
			||||||
 | 
					      _temporaryPlanSave();
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (doLoadFromTemporary) _temporaryLoad();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String mode = kTitleMap.keys.first;
 | 
					  String mode = kTitleMap.keys.first;
 | 
				
			||||||
@@ -176,9 +195,12 @@ 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;
 | 
				
			||||||
 | 
					  SnAttachment? videoAttachment;
 | 
				
			||||||
 | 
					  SnPoll? poll;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> fetchRelatedPost(
 | 
					  Future<void> fetchRelatedPost(
 | 
				
			||||||
    BuildContext context, {
 | 
					    BuildContext context, {
 | 
				
			||||||
@@ -198,13 +220,18 @@ 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 ?? '';
 | 
				
			||||||
 | 
					        rewardController.text = post.body['reward']?.toString() ?? '';
 | 
				
			||||||
 | 
					        videoAttachment = post.preload?.video;
 | 
				
			||||||
        publishedAt = post.publishedAt;
 | 
					        publishedAt = post.publishedAt;
 | 
				
			||||||
        publishedUntil = post.publishedUntil;
 | 
					        publishedUntil = post.publishedUntil;
 | 
				
			||||||
        visibleUsers = List.from(post.visibleUsersList ?? []);
 | 
					        visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
 | 
				
			||||||
        invisibleUsers = List.from(post.invisibleUsersList ?? []);
 | 
					        invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
 | 
				
			||||||
        visibility = post.visibility;
 | 
					        visibility = post.visibility;
 | 
				
			||||||
        tags = List.from(post.tags.map((ele) => ele.alias));
 | 
					        tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
 | 
				
			||||||
 | 
					        categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
 | 
				
			||||||
        attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
 | 
					        attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
 | 
				
			||||||
 | 
					        poll = post.preload?.poll;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
 | 
					        if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
 | 
				
			||||||
          thumbnail = PostWriteMedia(post.preload!.thumbnail);
 | 
					          thumbnail = PostWriteMedia(post.preload!.thumbnail);
 | 
				
			||||||
@@ -231,7 +258,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media) async {
 | 
					  Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
 | 
				
			||||||
 | 
					      {bool isCompressed = false}) async {
 | 
				
			||||||
    final attach = context.read<SnAttachmentProvider>();
 | 
					    final attach = context.read<SnAttachmentProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final place = await attach.chunkedUploadInitialize(
 | 
					    final place = await attach.chunkedUploadInitialize(
 | 
				
			||||||
@@ -239,22 +267,145 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
      media.name,
 | 
					      media.name,
 | 
				
			||||||
      'interactive',
 | 
					      'interactive',
 | 
				
			||||||
      null,
 | 
					      null,
 | 
				
			||||||
      mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
 | 
					      mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final item = await attach.chunkedUploadParts(
 | 
					    var item = await attach.chunkedUploadParts(
 | 
				
			||||||
      media.toFile()!,
 | 
					      media.toFile()!,
 | 
				
			||||||
      place.$1,
 | 
					      place.$1,
 | 
				
			||||||
      place.$2,
 | 
					      place.$2,
 | 
				
			||||||
      onProgress: (progress) {
 | 
					      analyzeNow: media.type == SnMediaType.image,
 | 
				
			||||||
        progress = progress;
 | 
					      onProgress: (value) {
 | 
				
			||||||
 | 
					        progress = value;
 | 
				
			||||||
        notifyListeners();
 | 
					        notifyListeners();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        final compressedAttachment = await _tryCompressVideoCopy(context, media);
 | 
				
			||||||
 | 
					        if (compressedAttachment != null) {
 | 
				
			||||||
 | 
					          item = await attach.updateOne(item, compressedId: compressedAttachment.id);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        if (context.mounted) context.showErrorDialog(err);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return item;
 | 
					    return item;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
 | 
				
			||||||
 | 
					    if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
 | 
				
			||||||
 | 
					    if (media.type != SnMediaType.video) return null;
 | 
				
			||||||
 | 
					    if (media.file == null) return null;
 | 
				
			||||||
 | 
					    if (VideoCompress.isCompressing) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final confirm = await context.showConfirmDialog(
 | 
				
			||||||
 | 
					      'attachmentVideoCompressHint'.tr(),
 | 
				
			||||||
 | 
					      'attachmentVideoCompressHintDescription'.tr(args: [media.file!.name]),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (!confirm) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    progress = null;
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final mediaInfo = await VideoCompress.compressVideo(
 | 
				
			||||||
 | 
					      media.file!.path,
 | 
				
			||||||
 | 
					      quality: VideoQuality.LowQuality,
 | 
				
			||||||
 | 
					      frameRate: 30,
 | 
				
			||||||
 | 
					      deleteOrigin: false,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (mediaInfo == null) return null;
 | 
				
			||||||
 | 
					    if (!context.mounted) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
 | 
				
			||||||
 | 
					    final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return compressedAttachment;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const kTemporaryStorageKey = 'int_draft_post';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Timer? _temporarySaveTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _temporaryPlanSave() {
 | 
				
			||||||
 | 
					    if (!_temporarySaveActive) return;
 | 
				
			||||||
 | 
					    _temporarySaveTimer?.cancel();
 | 
				
			||||||
 | 
					    _temporarySaveTimer = Timer(const Duration(seconds: 1), () {
 | 
				
			||||||
 | 
					      _temporarySave();
 | 
				
			||||||
 | 
					      log("[PostWriter] Temporary save saved.");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _temporarySave() {
 | 
				
			||||||
 | 
					    SharedPreferences.getInstance().then((prefs) {
 | 
				
			||||||
 | 
					      if (titleController.text.isEmpty &&
 | 
				
			||||||
 | 
					          descriptionController.text.isEmpty &&
 | 
				
			||||||
 | 
					          contentController.text.isEmpty &&
 | 
				
			||||||
 | 
					          thumbnail == null &&
 | 
				
			||||||
 | 
					          attachments.isEmpty) {
 | 
				
			||||||
 | 
					        prefs.remove(kTemporaryStorageKey);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      prefs.setString(
 | 
				
			||||||
 | 
					        kTemporaryStorageKey,
 | 
				
			||||||
 | 
					        jsonEncode({
 | 
				
			||||||
 | 
					          'publisher': publisher,
 | 
				
			||||||
 | 
					          'content': contentController.text,
 | 
				
			||||||
 | 
					          if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
 | 
				
			||||||
 | 
					          if (titleController.text.isNotEmpty) 'title': titleController.text,
 | 
				
			||||||
 | 
					          if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
 | 
				
			||||||
 | 
					          if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
 | 
				
			||||||
 | 
					          if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
 | 
				
			||||||
 | 
					          'attachments':
 | 
				
			||||||
 | 
					              attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
 | 
				
			||||||
 | 
					          'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
 | 
				
			||||||
 | 
					          'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
 | 
				
			||||||
 | 
					          'visibility': visibility,
 | 
				
			||||||
 | 
					          'visible_users_list': visibleUsers,
 | 
				
			||||||
 | 
					          'invisible_users_list': invisibleUsers,
 | 
				
			||||||
 | 
					          if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
 | 
				
			||||||
 | 
					          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
				
			||||||
 | 
					          if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
 | 
				
			||||||
 | 
					          if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
 | 
				
			||||||
 | 
					          if (poll != null) 'poll': poll!.toJson(),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool temporaryRestored = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _temporaryLoad() {
 | 
				
			||||||
 | 
					    SharedPreferences.getInstance().then((prefs) {
 | 
				
			||||||
 | 
					      final raw = prefs.getString(kTemporaryStorageKey);
 | 
				
			||||||
 | 
					      if (raw == null) return;
 | 
				
			||||||
 | 
					      final data = jsonDecode(raw);
 | 
				
			||||||
 | 
					      contentController.text = data['content'];
 | 
				
			||||||
 | 
					      aliasController.text = data['alias'] ?? '';
 | 
				
			||||||
 | 
					      titleController.text = data['title'] ?? '';
 | 
				
			||||||
 | 
					      descriptionController.text = data['description'] ?? '';
 | 
				
			||||||
 | 
					      rewardController.text = data['reward']?.toString() ?? '';
 | 
				
			||||||
 | 
					      if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
 | 
				
			||||||
 | 
					      attachments
 | 
				
			||||||
 | 
					          .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
 | 
				
			||||||
 | 
					      tags = List.from(data['tags'].map((ele) => ele['alias']));
 | 
				
			||||||
 | 
					      categories = List.from(data['categories'].map((ele) => ele['alias']));
 | 
				
			||||||
 | 
					      visibility = data['visibility'];
 | 
				
			||||||
 | 
					      visibleUsers = List.from(data['visible_users_list'] ?? []);
 | 
				
			||||||
 | 
					      invisibleUsers = List.from(data['invisible_users_list'] ?? []);
 | 
				
			||||||
 | 
					      if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
 | 
				
			||||||
 | 
					      if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
 | 
				
			||||||
 | 
					      replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
 | 
				
			||||||
 | 
					      repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
 | 
				
			||||||
 | 
					      poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
 | 
				
			||||||
 | 
					      temporaryRestored = true;
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
 | 
					  Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
 | 
				
			||||||
    if (isBusy) return;
 | 
					    if (isBusy) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -269,7 +420,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>();
 | 
				
			||||||
@@ -296,21 +447,34 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
          media.name,
 | 
					          media.name,
 | 
				
			||||||
          'interactive',
 | 
					          'interactive',
 | 
				
			||||||
          null,
 | 
					          null,
 | 
				
			||||||
          mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
 | 
					          mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final item = await attach.chunkedUploadParts(
 | 
					        var item = await attach.chunkedUploadParts(
 | 
				
			||||||
          media.toFile()!,
 | 
					          media.toFile()!,
 | 
				
			||||||
          place.$1,
 | 
					          place.$1,
 | 
				
			||||||
          place.$2,
 | 
					          place.$2,
 | 
				
			||||||
          onProgress: (progress) {
 | 
					          onProgress: (value) {
 | 
				
			||||||
            // Calculate overall progress for attachments
 | 
					            // Calculate overall progress for attachments
 | 
				
			||||||
            progress = ((i + progress) / attachments.length) * kAttachmentProgressWeight;
 | 
					            progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
 | 
				
			||||||
            notifyListeners();
 | 
					            notifyListeners();
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          if (context.mounted) {
 | 
				
			||||||
 | 
					            final compressedAttachment = await _tryCompressVideoCopy(context, media);
 | 
				
			||||||
 | 
					            if (compressedAttachment != null) {
 | 
				
			||||||
 | 
					              item = await attach.updateOne(item, compressedId: compressedAttachment.id);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					          if (context.mounted) context.showErrorDialog(err);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
 | 
				
			||||||
        attachments[i] = PostWriteMedia(item);
 | 
					        attachments[i] = PostWriteMedia(item);
 | 
				
			||||||
 | 
					        notifyListeners();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      isBusy = false;
 | 
					      isBusy = false;
 | 
				
			||||||
@@ -323,6 +487,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    progress = kAttachmentProgressWeight;
 | 
					    progress = kAttachmentProgressWeight;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final reward = double.tryParse(rewardController.text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Posting the content
 | 
					    // Posting the content
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final baseProgressVal = progress!;
 | 
					      final baseProgressVal = progress!;
 | 
				
			||||||
@@ -334,11 +500,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,
 | 
				
			||||||
@@ -346,6 +514,9 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
					          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
				
			||||||
          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
					          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
				
			||||||
          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
					          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
				
			||||||
 | 
					          if (reward != null) 'reward': reward,
 | 
				
			||||||
 | 
					          if (videoAttachment != null) 'video': videoAttachment!.rid,
 | 
				
			||||||
 | 
					          if (poll != null) 'poll': poll!.id,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        onSendProgress: (count, total) {
 | 
					        onSendProgress: (count, total) {
 | 
				
			||||||
          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
					          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
				
			||||||
@@ -359,6 +530,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
          method: editingPost != null ? 'PUT' : 'POST',
 | 
					          method: editingPost != null ? 'PUT' : 'POST',
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					      reset();
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (!context.mounted) return;
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
@@ -407,65 +579,98 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void setPublisher(SnPublisher? item) {
 | 
					  void setPublisher(SnPublisher? item) {
 | 
				
			||||||
    publisher = item;
 | 
					    publisher = item;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setPublishedAt(DateTime? value) {
 | 
					  void setPublishedAt(DateTime? value) {
 | 
				
			||||||
    publishedAt = value;
 | 
					    publishedAt = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setPublishedUntil(DateTime? value) {
 | 
					  void setPublishedUntil(DateTime? value) {
 | 
				
			||||||
    publishedUntil = value;
 | 
					    publishedUntil = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setTags(List<String> value) {
 | 
					  void setTags(List<String> value) {
 | 
				
			||||||
    tags = value;
 | 
					    tags = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setCategories(List<String> value) {
 | 
				
			||||||
 | 
					    categories = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setVisibility(int value) {
 | 
					  void setVisibility(int value) {
 | 
				
			||||||
    visibility = value;
 | 
					    visibility = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setVisibleUsers(List<int> value) {
 | 
					  void setVisibleUsers(List<int> value) {
 | 
				
			||||||
    visibleUsers = value;
 | 
					    visibleUsers = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setInvisibleUsers(List<int> value) {
 | 
					  void setInvisibleUsers(List<int> value) {
 | 
				
			||||||
    invisibleUsers = value;
 | 
					    invisibleUsers = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setProgress(double? value) {
 | 
					  void setProgress(double? value) {
 | 
				
			||||||
    progress = value;
 | 
					    progress = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setIsBusy(bool value) {
 | 
					  void setIsBusy(bool value) {
 | 
				
			||||||
    isBusy = value;
 | 
					    isBusy = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setMode(String value) {
 | 
					  void setMode(String value) {
 | 
				
			||||||
    mode = value;
 | 
					    mode = value;
 | 
				
			||||||
 | 
					    _temporaryPlanSave();
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setVideoAttachment(SnAttachment? value) {
 | 
				
			||||||
 | 
					    videoAttachment = value;
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setPoll(SnPoll? value) {
 | 
				
			||||||
 | 
					    poll = value;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void reset() {
 | 
					  void reset() {
 | 
				
			||||||
    publishedAt = null;
 | 
					    publishedAt = null;
 | 
				
			||||||
    publishedUntil = null;
 | 
					    publishedUntil = null;
 | 
				
			||||||
 | 
					    thumbnail = null;
 | 
				
			||||||
 | 
					    visibility = 0;
 | 
				
			||||||
    titleController.clear();
 | 
					    titleController.clear();
 | 
				
			||||||
    descriptionController.clear();
 | 
					    descriptionController.clear();
 | 
				
			||||||
    contentController.clear();
 | 
					    contentController.clear();
 | 
				
			||||||
    attachments.clear();
 | 
					    aliasController.clear();
 | 
				
			||||||
 | 
					    tags = List.empty(growable: true);
 | 
				
			||||||
 | 
					    categories = List.empty(growable: true);
 | 
				
			||||||
 | 
					    attachments = List.empty(growable: true);
 | 
				
			||||||
    editingPost = null;
 | 
					    editingPost = null;
 | 
				
			||||||
    replyingPost = null;
 | 
					    replyingPost = null;
 | 
				
			||||||
    repostingPost = null;
 | 
					    repostingPost = null;
 | 
				
			||||||
    mode = kTitleMap.keys.first;
 | 
					    mode = kTitleMap.keys.first;
 | 
				
			||||||
 | 
					    temporaryRestored = false;
 | 
				
			||||||
 | 
					    SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -474,6 +679,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    contentController.dispose();
 | 
					    contentController.dispose();
 | 
				
			||||||
    titleController.dispose();
 | 
					    titleController.dispose();
 | 
				
			||||||
    descriptionController.dispose();
 | 
					    descriptionController.dispose();
 | 
				
			||||||
 | 
					    aliasController.dispose();
 | 
				
			||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										272
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						@@ -1,23 +1,29 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:developer';
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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';
 | 
				
			||||||
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/services.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:hotkey_manager/hotkey_manager.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:styled_widget/styled_widget.dart';
 | 
					import 'package:shared_preferences/shared_preferences.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 +31,8 @@ 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/sn_sticker.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';
 | 
				
			||||||
@@ -35,8 +43,27 @@ import 'package:surface/types/chat.dart';
 | 
				
			|||||||
import 'package:surface/types/realm.dart';
 | 
					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:tray_manager/tray_manager.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 +91,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,25 +129,32 @@ 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)),
 | 
				
			||||||
            Provider(create: (ctx) => SnRelationshipProvider(ctx)),
 | 
					            Provider(create: (ctx) => SnRelationshipProvider(ctx)),
 | 
				
			||||||
            Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
 | 
					            Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
 | 
				
			||||||
 | 
					            Provider(create: (ctx) => SnStickerProvider(ctx)),
 | 
				
			||||||
            ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
 | 
					            ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
 | 
				
			||||||
            ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
 | 
					            ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
 | 
				
			||||||
            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(),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -160,18 +210,71 @@ class _AppSplashScreen extends StatefulWidget {
 | 
				
			|||||||
  State<_AppSplashScreen> createState() => _AppSplashScreenState();
 | 
					  State<_AppSplashScreen> createState() => _AppSplashScreenState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
 | 
					class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
				
			||||||
  bool _isReady = false;
 | 
					  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());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  late StreamSubscription _shareIntentSubscription;
 | 
					  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 cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					      WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
				
			||||||
 | 
					        cfg.calcDrawerSize(context);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      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();
 | 
				
			||||||
@@ -180,46 +283,141 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
 | 
				
			|||||||
      await ws.tryConnect();
 | 
					      await ws.tryConnect();
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
      final notify = context.read<NotificationProvider>();
 | 
					      final notify = context.read<NotificationProvider>();
 | 
				
			||||||
 | 
					      notify.listen();
 | 
				
			||||||
      await notify.registerPushNotifications();
 | 
					      await notify.registerPushNotifications();
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      final sticker = context.read<SnStickerProvider>();
 | 
				
			||||||
 | 
					      await sticker.listStickerEagerly();
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
      await context.showErrorDialog(err);
 | 
					      await context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      setState(() => _isReady = true);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _postInitialization() async {
 | 
				
			||||||
 | 
					    await widgetUpdateRandomPost();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _hotkeyInitialization() async {
 | 
				
			||||||
 | 
					    if (kIsWeb) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (Platform.isMacOS) {
 | 
				
			||||||
 | 
					      HotKey quitHotKey = HotKey(
 | 
				
			||||||
 | 
					        key: PhysicalKeyboardKey.keyQ,
 | 
				
			||||||
 | 
					        modifiers: [HotKeyModifier.meta],
 | 
				
			||||||
 | 
					        scope: HotKeyScope.inapp,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      await hotKeyManager.register(quitHotKey, keyUpHandler: (_) {
 | 
				
			||||||
 | 
					        _appLifecycleListener?.dispose();
 | 
				
			||||||
 | 
					        SystemChannels.platform.invokeMethod('SystemNavigator.pop');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _trayInitialization() async {
 | 
				
			||||||
 | 
					    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
 | 
				
			||||||
 | 
					    final appVersion = await PackageInfo.fromPlatform();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    trayManager.addListener(this);
 | 
				
			||||||
 | 
					    await trayManager.setIcon(icon);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Menu menu = Menu(
 | 
				
			||||||
 | 
					      items: [
 | 
				
			||||||
 | 
					        MenuItem(
 | 
				
			||||||
 | 
					          key: 'version_label',
 | 
				
			||||||
 | 
					          label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
 | 
				
			||||||
 | 
					          disabled: true,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        MenuItem.separator(),
 | 
				
			||||||
 | 
					        MenuItem(
 | 
				
			||||||
 | 
					          key: 'exit',
 | 
				
			||||||
 | 
					          label: 'trayMenuExit'.tr(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await trayManager.setContextMenu(menu);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AppLifecycleListener? _appLifecycleListener;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
    _initialize();
 | 
					
 | 
				
			||||||
 | 
					    if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
 | 
				
			||||||
 | 
					      _appLifecycleListener = AppLifecycleListener(
 | 
				
			||||||
 | 
					        onExitRequested: _onExitRequested,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _trayInitialization();
 | 
				
			||||||
 | 
					    _hotkeyInitialization();
 | 
				
			||||||
 | 
					    _initialize().then((_) {
 | 
				
			||||||
 | 
					      _postInitialization();
 | 
				
			||||||
 | 
					      _tryRequestRating();
 | 
				
			||||||
 | 
					      _checkForUpdate();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<AppExitResponse> _onExitRequested() async {
 | 
				
			||||||
 | 
					    appWindow.hide();
 | 
				
			||||||
 | 
					    return AppExitResponse.cancel;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void onTrayIconMouseDown() {
 | 
				
			||||||
 | 
					    if (Platform.isWindows) {
 | 
				
			||||||
 | 
					      context.read<NotificationProvider>().clearTray();
 | 
				
			||||||
 | 
					      appWindow.show();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      trayManager.popUpContextMenu();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void onTrayIconRightMouseDown() {
 | 
				
			||||||
 | 
					    if (Platform.isWindows) {
 | 
				
			||||||
 | 
					      trayManager.popUpContextMenu();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      context.read<NotificationProvider>().clearTray();
 | 
				
			||||||
 | 
					      appWindow.show();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void onTrayMenuItemClick(MenuItem menuItem) {
 | 
				
			||||||
 | 
					    switch (menuItem.key) {
 | 
				
			||||||
 | 
					      case 'exit':
 | 
				
			||||||
 | 
					        _appLifecycleListener?.dispose();
 | 
				
			||||||
 | 
					        SystemChannels.platform.invokeMethod('SystemNavigator.pop');
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
 | 
				
			||||||
 | 
					      trayManager.removeListener(this);
 | 
				
			||||||
 | 
					      hotKeyManager.unregisterAll();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    if (!_isReady) {
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
      return Scaffold(
 | 
					    return NotificationListener<SizeChangedLayoutNotification>(
 | 
				
			||||||
        backgroundColor: Theme.of(context).colorScheme.surface,
 | 
					      onNotification: (notification) {
 | 
				
			||||||
        body: Container(
 | 
					        WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
				
			||||||
          constraints: const BoxConstraints(maxWidth: 180),
 | 
					          cfg.calcDrawerSize(context);
 | 
				
			||||||
          child: Column(
 | 
					        });
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
					        return false;
 | 
				
			||||||
            mainAxisSize: MainAxisSize.min,
 | 
					      },
 | 
				
			||||||
            children: [
 | 
					      child: SizeChangedLayoutNotifier(
 | 
				
			||||||
              Image.asset("assets/icon/icon.png", width: 64, height: 64),
 | 
					        child: widget.child,
 | 
				
			||||||
              const Gap(6),
 | 
					      ),
 | 
				
			||||||
              LinearProgressIndicator(
 | 
					    );
 | 
				
			||||||
                backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Gap(20),
 | 
					 | 
				
			||||||
              Text('appInitializing'.tr(), textAlign: TextAlign.center),
 | 
					 | 
				
			||||||
              AppVersionLabel(),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ).center(),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return widget.child;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										86
									
								
								lib/providers/config.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:responsive_framework/responsive_framework.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 kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
 | 
				
			||||||
 | 
					const kAppNotifyWithHaptic = 'app_notify_with_haptic';
 | 
				
			||||||
 | 
					const kAppExpandPostLink = 'app_expand_post_link';
 | 
				
			||||||
 | 
					const kAppExpandChatLink = 'app_expand_chat_link';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool drawerIsCollapsed = false;
 | 
				
			||||||
 | 
					  bool drawerIsExpanded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
 | 
				
			||||||
 | 
					    bool newDrawerIsCollapsed = false;
 | 
				
			||||||
 | 
					    bool newDrawerIsExpanded = false;
 | 
				
			||||||
 | 
					    if (withMediaQuery) {
 | 
				
			||||||
 | 
					      newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
 | 
				
			||||||
 | 
					      newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      final rpb = ResponsiveBreakpoints.of(context);
 | 
				
			||||||
 | 
					      newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
 | 
				
			||||||
 | 
					      newDrawerIsExpanded = rpb.largerThan(TABLET)
 | 
				
			||||||
 | 
					          ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
 | 
				
			||||||
 | 
					              ? false
 | 
				
			||||||
 | 
					              : true
 | 
				
			||||||
 | 
					          : false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
 | 
				
			||||||
 | 
					      drawerIsExpanded = newDrawerIsExpanded;
 | 
				
			||||||
 | 
					      drawerIsCollapsed = newDrawerIsCollapsed;
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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())}';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
      screen: 'realm',
 | 
					      screen: 'realm',
 | 
				
			||||||
      label: 'screenRealm',
 | 
					      label: 'screenRealm',
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    AppNavDestination(
 | 
				
			||||||
 | 
					      icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
 | 
				
			||||||
 | 
					      screen: 'news',
 | 
				
			||||||
 | 
					      label: 'screenNews',
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    AppNavDestination(
 | 
					    AppNavDestination(
 | 
				
			||||||
      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
					      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
				
			||||||
      screen: 'album',
 | 
					      screen: 'album',
 | 
				
			||||||
@@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  List<AppNavDestination> destinations = [];
 | 
					  List<AppNavDestination> destinations = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int get pinnedDestinationCount =>
 | 
					  int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
 | 
				
			||||||
      destinations.where((ele) => ele.isPinned).length;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  NavigationProvider() {
 | 
					  NavigationProvider() {
 | 
				
			||||||
    buildDestinations(kDefaultPinnedDestination);
 | 
					    buildDestinations(kDefaultPinnedDestination);
 | 
				
			||||||
@@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool isIndexInRange(int min, int max) {
 | 
					  bool isIndexInRange(int min, int max) {
 | 
				
			||||||
    return _currentIndex != null &&
 | 
					    return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
 | 
				
			||||||
        _currentIndex! >= min &&
 | 
					 | 
				
			||||||
        _currentIndex! < max;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void autoDetectIndex(GoRouter? state) {
 | 
					  void autoDetectIndex(GoRouter? state) {
 | 
				
			||||||
    if (state == null) return;
 | 
					    if (state == null) return;
 | 
				
			||||||
    final idx = destinations.indexWhere(
 | 
					    final idx = destinations.indexWhere(
 | 
				
			||||||
      (ele) =>
 | 
					      (ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
 | 
				
			||||||
          ele.screen ==
 | 
					 | 
				
			||||||
          state.routerDelegate.currentConfiguration.last.route.name,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    _currentIndex = idx == -1 ? null : idx;
 | 
					    _currentIndex = idx == -1 ? null : idx;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,18 +4,27 @@ import 'dart:io';
 | 
				
			|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
					import 'package:firebase_messaging/firebase_messaging.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter_udid/flutter_udid.dart';
 | 
					import 'package:flutter_udid/flutter_udid.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.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/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/websocket.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/notification.dart';
 | 
				
			||||||
 | 
					import 'package:tray_manager/tray_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NotificationProvider extends ChangeNotifier {
 | 
					class NotificationProvider extends ChangeNotifier {
 | 
				
			||||||
  late final SnNetworkProvider _sn;
 | 
					  late final SnNetworkProvider _sn;
 | 
				
			||||||
  late final UserProvider _ua;
 | 
					  late final UserProvider _ua;
 | 
				
			||||||
 | 
					  late final WebSocketProvider _ws;
 | 
				
			||||||
 | 
					  late final ConfigProvider _cfg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  NotificationProvider(BuildContext context) {
 | 
					  NotificationProvider(BuildContext context) {
 | 
				
			||||||
    _sn = context.read<SnNetworkProvider>();
 | 
					    _sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
    _ua = context.read<UserProvider>();
 | 
					    _ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					    _ws = context.read<WebSocketProvider>();
 | 
				
			||||||
 | 
					    _cfg = context.read<ConfigProvider>();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> registerPushNotifications() async {
 | 
					  Future<void> registerPushNotifications() async {
 | 
				
			||||||
@@ -62,4 +71,49 @@ class NotificationProvider extends ChangeNotifier {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int showingCount = 0;
 | 
				
			||||||
 | 
					  int showingTrayCount = 0;
 | 
				
			||||||
 | 
					  List<SnNotification> notifications = List.empty(growable: true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void listen() {
 | 
				
			||||||
 | 
					    _ws.pk.stream.listen((event) {
 | 
				
			||||||
 | 
					      if (event.method == 'notifications.new') {
 | 
				
			||||||
 | 
					        final notification = SnNotification.fromJson(event.payload!);
 | 
				
			||||||
 | 
					        if (showingCount < 0) showingCount = 0;
 | 
				
			||||||
 | 
					        showingCount++;
 | 
				
			||||||
 | 
					        showingTrayCount++;
 | 
				
			||||||
 | 
					        notifications.add(notification);
 | 
				
			||||||
 | 
					        Future.delayed(const Duration(seconds: 3), () {
 | 
				
			||||||
 | 
					          if (showingCount >= 0) showingCount--;
 | 
				
			||||||
 | 
					          notifyListeners();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        notifyListeners();
 | 
				
			||||||
 | 
					        updateTray();
 | 
				
			||||||
 | 
					        final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
 | 
				
			||||||
 | 
					        if (doHaptic) HapticFeedback.mediumImpact();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void clearTray() {
 | 
				
			||||||
 | 
					    showingTrayCount = 0;
 | 
				
			||||||
 | 
					    updateTray();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void updateTray() {
 | 
				
			||||||
 | 
					    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
				
			||||||
 | 
					    if (showingTrayCount == 0) {
 | 
				
			||||||
 | 
					      trayManager.setTitle('');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      trayManager.setTitle(' $showingTrayCount');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void clear() {
 | 
				
			||||||
 | 
					    showingCount = 0;
 | 
				
			||||||
 | 
					    notifications.clear();
 | 
				
			||||||
 | 
					    updateTray();
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.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/user_directory.dart';
 | 
					import 'package:surface/providers/user_directory.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/poll.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SnPostContentProvider {
 | 
					class SnPostContentProvider {
 | 
				
			||||||
@@ -16,6 +17,11 @@ class SnPostContentProvider {
 | 
				
			|||||||
    _attach = context.read<SnAttachmentProvider>();
 | 
					    _attach = context.read<SnAttachmentProvider>();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<SnPoll> _fetchPoll(int id) async {
 | 
				
			||||||
 | 
					    final resp = await _sn.client.get('/cgi/co/polls/$id');
 | 
				
			||||||
 | 
					    return SnPoll.fromJson(resp.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
 | 
					  Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
 | 
				
			||||||
    Set<String> rids = {};
 | 
					    Set<String> rids = {};
 | 
				
			||||||
    for (var i = 0; i < out.length; i++) {
 | 
					    for (var i = 0; i < out.length; i++) {
 | 
				
			||||||
@@ -23,6 +29,9 @@ class SnPostContentProvider {
 | 
				
			|||||||
      if (out[i].body['thumbnail'] != null) {
 | 
					      if (out[i].body['thumbnail'] != null) {
 | 
				
			||||||
        rids.add(out[i].body['thumbnail']);
 | 
					        rids.add(out[i].body['thumbnail']);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if (out[i].body['video'] != null) {
 | 
				
			||||||
 | 
					        rids.add(out[i].body['video']);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      if (out[i].repostTo != null) {
 | 
					      if (out[i].repostTo != null) {
 | 
				
			||||||
        out[i] = out[i].copyWith(
 | 
					        out[i] = out[i].copyWith(
 | 
				
			||||||
          repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
 | 
					          repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
 | 
				
			||||||
@@ -32,10 +41,17 @@ class SnPostContentProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final attachments = await _attach.getMultiple(rids.toList());
 | 
					    final attachments = await _attach.getMultiple(rids.toList());
 | 
				
			||||||
    for (var i = 0; i < out.length; i++) {
 | 
					    for (var i = 0; i < out.length; i++) {
 | 
				
			||||||
 | 
					      SnPoll? poll;
 | 
				
			||||||
 | 
					      if (out[i].pollId != null) {
 | 
				
			||||||
 | 
					        poll = await _fetchPoll(out[i].pollId!);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      out[i] = out[i].copyWith(
 | 
					      out[i] = out[i].copyWith(
 | 
				
			||||||
        preload: SnPostPreload(
 | 
					        preload: SnPostPreload(
 | 
				
			||||||
          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
					          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
				
			||||||
          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
					          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
				
			||||||
 | 
					          video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
 | 
				
			||||||
 | 
					          poll: poll,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -53,6 +69,9 @@ class SnPostContentProvider {
 | 
				
			|||||||
    if (out.body['thumbnail'] != null) {
 | 
					    if (out.body['thumbnail'] != null) {
 | 
				
			||||||
      rids.add(out.body['thumbnail']);
 | 
					      rids.add(out.body['thumbnail']);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (out.body['video'] != null) {
 | 
				
			||||||
 | 
					      rids.add(out.body['video']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (out.repostTo != null) {
 | 
					    if (out.repostTo != null) {
 | 
				
			||||||
      out = out.copyWith(
 | 
					      out = out.copyWith(
 | 
				
			||||||
        repostTo: await _preloadRelatedDataSingle(out.repostTo!),
 | 
					        repostTo: await _preloadRelatedDataSingle(out.repostTo!),
 | 
				
			||||||
@@ -60,10 +79,18 @@ class SnPostContentProvider {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final attachments = await _attach.getMultiple(rids.toList());
 | 
					    final attachments = await _attach.getMultiple(rids.toList());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SnPoll? poll;
 | 
				
			||||||
 | 
					    if (out.pollId != null) {
 | 
				
			||||||
 | 
					      poll = await _fetchPoll(out.pollId!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    out = out.copyWith(
 | 
					    out = out.copyWith(
 | 
				
			||||||
      preload: SnPostPreload(
 | 
					      preload: SnPostPreload(
 | 
				
			||||||
        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
					        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
				
			||||||
        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
					        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
				
			||||||
 | 
					        video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
 | 
				
			||||||
 | 
					        poll: poll,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,12 +110,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 +149,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)) ?? []),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ class SnAttachmentProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
 | 
					  void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
 | 
				
			||||||
    for (final item in items) {
 | 
					    for (final item in items) {
 | 
				
			||||||
      if ((item.isAnalyzed && item.isUploaded) || noCheck) {
 | 
					      if (item.isAnalyzed || noCheck) {
 | 
				
			||||||
        _cache[item.rid] = item;
 | 
					        _cache[item.rid] = item;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -34,15 +34,14 @@ class SnAttachmentProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
 | 
					    final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
 | 
				
			||||||
    final out = SnAttachment.fromJson(resp.data);
 | 
					    final out = SnAttachment.fromJson(resp.data);
 | 
				
			||||||
    if (out.isAnalyzed && out.isUploaded) {
 | 
					    if (out.isAnalyzed) {
 | 
				
			||||||
      _cache[rid] = out;
 | 
					      _cache[rid] = out;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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,13 +62,12 @@ class SnAttachmentProvider {
 | 
				
			|||||||
          'id': pendingFetch.join(','),
 | 
					          'id': pendingFetch.join(','),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      final out = resp.data['data']
 | 
					      final List<SnAttachment?> out =
 | 
				
			||||||
          .map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
 | 
					          resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
 | 
				
			||||||
          .toList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (final item in out) {
 | 
					      for (final item in out) {
 | 
				
			||||||
        if (item == null) continue;
 | 
					        if (item == null) continue;
 | 
				
			||||||
        if (item.isAnalyzed && item.isUploaded) {
 | 
					        if (item.isAnalyzed) {
 | 
				
			||||||
          _cache[item.rid] = item;
 | 
					          _cache[item.rid] = item;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        result[randomMapping[item.rid]!] = item;
 | 
					        result[randomMapping[item.rid]!] = item;
 | 
				
			||||||
@@ -79,10 +77,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,
 | 
				
			||||||
@@ -91,13 +86,11 @@ class SnAttachmentProvider {
 | 
				
			|||||||
    Map<String, dynamic>? metadata, {
 | 
					    Map<String, dynamic>? metadata, {
 | 
				
			||||||
    String? mimetype,
 | 
					    String? mimetype,
 | 
				
			||||||
    Function(double progress)? onProgress,
 | 
					    Function(double progress)? onProgress,
 | 
				
			||||||
 | 
					    bool analyzeNow = false,
 | 
				
			||||||
  }) 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) {
 | 
				
			||||||
@@ -116,6 +109,7 @@ class SnAttachmentProvider {
 | 
				
			|||||||
    final resp = await _sn.client.post(
 | 
					    final resp = await _sn.client.post(
 | 
				
			||||||
      '/cgi/uc/attachments',
 | 
					      '/cgi/uc/attachments',
 | 
				
			||||||
      data: formData,
 | 
					      data: formData,
 | 
				
			||||||
 | 
					      queryParameters: {'analyzeNow': analyzeNow},
 | 
				
			||||||
      onSendProgress: (count, total) {
 | 
					      onSendProgress: (count, total) {
 | 
				
			||||||
        if (onProgress != null) {
 | 
					        if (onProgress != null) {
 | 
				
			||||||
          onProgress(count / total);
 | 
					          onProgress(count / total);
 | 
				
			||||||
@@ -126,18 +120,15 @@ class SnAttachmentProvider {
 | 
				
			|||||||
    return SnAttachment.fromJson(resp.data);
 | 
					    return SnAttachment.fromJson(resp.data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<(SnAttachment, int)> chunkedUploadInitialize(
 | 
					  Future<(SnAttachmentFragment, int)> chunkedUploadInitialize(
 | 
				
			||||||
    int size,
 | 
					    int size,
 | 
				
			||||||
    String filename,
 | 
					    String filename,
 | 
				
			||||||
    String pool,
 | 
					    String pool,
 | 
				
			||||||
    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)) {
 | 
				
			||||||
@@ -146,7 +137,7 @@ class SnAttachmentProvider {
 | 
				
			|||||||
      mimetypeOverride = mimetype;
 | 
					      mimetypeOverride = mimetype;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final resp = await _sn.client.post('/cgi/uc/attachments/multipart', data: {
 | 
					    final resp = await _sn.client.post('/cgi/uc/fragments', data: {
 | 
				
			||||||
      'alt': fileAlt,
 | 
					      'alt': fileAlt,
 | 
				
			||||||
      'name': filename,
 | 
					      'name': filename,
 | 
				
			||||||
      'pool': pool,
 | 
					      'pool': pool,
 | 
				
			||||||
@@ -155,21 +146,20 @@ class SnAttachmentProvider {
 | 
				
			|||||||
      if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
 | 
					      if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (SnAttachmentFragment.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<dynamic> _chunkedUploadOnePart(
 | 
				
			||||||
    Uint8List data,
 | 
					    Uint8List data,
 | 
				
			||||||
    String rid,
 | 
					    String rid,
 | 
				
			||||||
    String cid, {
 | 
					    String cid, {
 | 
				
			||||||
    Function(double progress)? onProgress,
 | 
					    Function(double progress)? onProgress,
 | 
				
			||||||
 | 
					    bool analyzeNow = false,
 | 
				
			||||||
  }) async {
 | 
					  }) async {
 | 
				
			||||||
    final resp = await _sn.client.post(
 | 
					    final resp = await _sn.client.post(
 | 
				
			||||||
      '/cgi/uc/attachments/multipart/$rid/$cid',
 | 
					      '/cgi/uc/fragments/$rid/$cid',
 | 
				
			||||||
      data: data,
 | 
					      data: data,
 | 
				
			||||||
 | 
					      queryParameters: {'analyzeNow': analyzeNow},
 | 
				
			||||||
      options: Options(headers: {'Content-Type': 'application/octet-stream'}),
 | 
					      options: Options(headers: {'Content-Type': 'application/octet-stream'}),
 | 
				
			||||||
      onSendProgress: (count, total) {
 | 
					      onSendProgress: (count, total) {
 | 
				
			||||||
        if (onProgress != null) {
 | 
					        if (onProgress != null) {
 | 
				
			||||||
@@ -178,21 +168,28 @@ class SnAttachmentProvider {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SnAttachment.fromJson(resp.data);
 | 
					    if (resp.data['attachment'] != null) {
 | 
				
			||||||
 | 
					      return SnAttachment.fromJson(resp.data['attachment']);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return SnAttachmentFragment.fromJson(resp.data['fragment']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<SnAttachment> chunkedUploadParts(
 | 
					  Future<SnAttachment> chunkedUploadParts(
 | 
				
			||||||
    XFile file,
 | 
					    XFile file,
 | 
				
			||||||
    SnAttachment place,
 | 
					    SnAttachmentFragment place,
 | 
				
			||||||
    int chunkSize, {
 | 
					    int chunkSize, {
 | 
				
			||||||
    Function(double progress)? onProgress,
 | 
					    Function(double progress)? onProgress,
 | 
				
			||||||
 | 
					    bool analyzeNow = false,
 | 
				
			||||||
  }) async {
 | 
					  }) async {
 | 
				
			||||||
    final Map<String, dynamic> chunks = place.fileChunks ?? {};
 | 
					    final Map<String, dynamic> chunks = place.fileChunks;
 | 
				
			||||||
    var currentTask = 0;
 | 
					    var completedTasks = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final queue = Queue<Future<void>>();
 | 
					    final queue = Queue<Future<void>>();
 | 
				
			||||||
    final activeTasks = <Future<void>>[];
 | 
					    final activeTasks = <Future<void>>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    late SnAttachment out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (final entry in chunks.entries) {
 | 
					    for (final entry in chunks.entries) {
 | 
				
			||||||
      queue.add(() async {
 | 
					      queue.add(() async {
 | 
				
			||||||
        final beginCursor = entry.value * chunkSize;
 | 
					        final beginCursor = entry.value * chunkSize;
 | 
				
			||||||
@@ -200,25 +197,28 @@ 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(
 | 
					        final result = await _chunkedUploadOnePart(
 | 
				
			||||||
          data,
 | 
					          data,
 | 
				
			||||||
          place.rid,
 | 
					          place.rid,
 | 
				
			||||||
          entry.key,
 | 
					          entry.key,
 | 
				
			||||||
          onProgress: (chunkProgress) {
 | 
					          analyzeNow: analyzeNow,
 | 
				
			||||||
            final overallProgress =
 | 
					          onProgress: (progress) {
 | 
				
			||||||
                (currentTask + chunkProgress) / chunks.length;
 | 
					            final overallProgress = (completedTasks + progress) / chunks.length;
 | 
				
			||||||
            if (onProgress != null) {
 | 
					            onProgress?.call(overallProgress);
 | 
				
			||||||
              onProgress(overallProgress);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        currentTask++;
 | 
					        completedTasks++;
 | 
				
			||||||
 | 
					        final overallProgress = completedTasks / chunks.length;
 | 
				
			||||||
 | 
					        onProgress?.call(overallProgress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (result is SnAttachmentFragment) {
 | 
				
			||||||
 | 
					          place = result;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          out = result as SnAttachment;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }());
 | 
					      }());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -235,6 +235,24 @@ class SnAttachmentProvider {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return place;
 | 
					    return out;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<SnAttachment> updateOne(
 | 
				
			||||||
 | 
					    SnAttachment item, {
 | 
				
			||||||
 | 
					    String? alt,
 | 
				
			||||||
 | 
					    int? thumbnailId,
 | 
				
			||||||
 | 
					    int? compressedId,
 | 
				
			||||||
 | 
					    Map<String, dynamic>? metadata,
 | 
				
			||||||
 | 
					    bool? isIndexable,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    final resp = await _sn.client.put('/cgi/uc/attachments/${item.id}', data: {
 | 
				
			||||||
 | 
					      'alt': alt ?? item.alt,
 | 
				
			||||||
 | 
					      'thumbnail': thumbnailId ?? item.thumbnailId,
 | 
				
			||||||
 | 
					      'compressed': compressedId ?? item.compressedId,
 | 
				
			||||||
 | 
					      'metadata': metadata ?? item.usermeta,
 | 
				
			||||||
 | 
					      'is_indexable': isIndexable ?? item.isIndexable,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return SnAttachment.fromJson(resp.data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										74
									
								
								lib/providers/sn_sticker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					import 'dart:developer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SnStickerProvider {
 | 
				
			||||||
 | 
					  late final SnNetworkProvider _sn;
 | 
				
			||||||
 | 
					  final Map<String, SnSticker?> _cache = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Map<int, List<SnSticker>> stickersByPack = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SnStickerProvider(BuildContext context) {
 | 
				
			||||||
 | 
					    _sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool hasNotSticker(String alias) {
 | 
				
			||||||
 | 
					    return _cache.containsKey(alias) && _cache[alias] == null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _cacheSticker(SnSticker sticker) {
 | 
				
			||||||
 | 
					    _cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
 | 
				
			||||||
 | 
					    if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true);
 | 
				
			||||||
 | 
					    if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<SnSticker?> lookupSticker(String alias) async {
 | 
				
			||||||
 | 
					    if (_cache.containsKey(alias)) {
 | 
				
			||||||
 | 
					      return _cache[alias];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
 | 
				
			||||||
 | 
					      final sticker = SnSticker.fromJson(resp.data);
 | 
				
			||||||
 | 
					      _cacheSticker(sticker);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return sticker;
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      _cache[alias] = null;
 | 
				
			||||||
 | 
					      log('[Sticker] Failed to lookup sticker $alias: $err');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> listStickerEagerly() async {
 | 
				
			||||||
 | 
					    var count = await listSticker();
 | 
				
			||||||
 | 
					    for (var page = 1; count > 0; count -= 10) {
 | 
				
			||||||
 | 
					      await listSticker(page: page);
 | 
				
			||||||
 | 
					      page++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<int> listSticker({int page = 0}) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: {
 | 
				
			||||||
 | 
					        'take': 10,
 | 
				
			||||||
 | 
					        'offset': page * 10,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      final data = resp.data;
 | 
				
			||||||
 | 
					      final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele));
 | 
				
			||||||
 | 
					      for (final sticker in stickers) {
 | 
				
			||||||
 | 
					        _cacheSticker(sticker);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return data['count'] as int;
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      log('[Sticker] Failed to list stickers: $err');
 | 
				
			||||||
 | 
					      rethrow;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										184
									
								
								lib/providers/special_day.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,184 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Stored as key: month, day
 | 
				
			||||||
 | 
					final Map<String, (int, int)> kSpecialDays = {
 | 
				
			||||||
 | 
					  // Birthday is dynamically generated according to the user's profile
 | 
				
			||||||
 | 
					  'NewYear': (1, 1),
 | 
				
			||||||
 | 
					  'LunarNewYear': (lunarToGregorian(null, 1, 1).month, lunarToGregorian(null, 1, 1).day),
 | 
				
			||||||
 | 
					  'MidAutumn': (lunarToGregorian(null, 8, 15).month, lunarToGregorian(null, 8, 15).day),
 | 
				
			||||||
 | 
					  'DragonBoat': (lunarToGregorian(null, 5, 5).month, lunarToGregorian(null, 5, 5).day),
 | 
				
			||||||
 | 
					  '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': '🎉',
 | 
				
			||||||
 | 
					  'LunarNewYear': '🎉',
 | 
				
			||||||
 | 
					  'MidAutumn': '🥮',
 | 
				
			||||||
 | 
					  'DragonBoat': '🐲',
 | 
				
			||||||
 | 
					  '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.add(-const Duration(days: 1)).difference(last).inSeconds.toDouble();
 | 
				
			||||||
 | 
					    final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble();
 | 
				
			||||||
 | 
					    return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final Map<int, LunarYear> lunarYearData = {
 | 
				
			||||||
 | 
					  2025: LunarYear(
 | 
				
			||||||
 | 
					    startDate: DateTime(2025, 1, 29),
 | 
				
			||||||
 | 
					    months: [29, 30, 30, 29, 30, 29, 29, 30, 30, 29, 30, 29],
 | 
				
			||||||
 | 
					    leapMonth: 0,
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LunarYear {
 | 
				
			||||||
 | 
					  final DateTime startDate;
 | 
				
			||||||
 | 
					  final List<int> months;
 | 
				
			||||||
 | 
					  final int leapMonth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  LunarYear({required this.startDate, required this.months, required this.leapMonth});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DateTime lunarToGregorian(int? year, int month, int day, {bool isLeapMonth = false}) {
 | 
				
			||||||
 | 
					  year = year ?? DateTime.now().year;
 | 
				
			||||||
 | 
					  final lunarYear = lunarYearData[year];
 | 
				
			||||||
 | 
					  if (lunarYear == null) {
 | 
				
			||||||
 | 
					    throw Exception('Lunar data for year $year not found');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int leapMonth = lunarYear.leapMonth;
 | 
				
			||||||
 | 
					  if (isLeapMonth && (leapMonth == 0 || leapMonth != month)) {
 | 
				
			||||||
 | 
					    throw Exception('Invalid leap month for year $year');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int daysFromStart = 0;
 | 
				
			||||||
 | 
					  for (int i = 0; i < month - 1; i++) {
 | 
				
			||||||
 | 
					    daysFromStart += lunarYear.months[i];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isLeapMonth) {
 | 
				
			||||||
 | 
					    daysFromStart += lunarYear.months[month - 1];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  daysFromStart += day - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return lunarYear.startDate.add(Duration(days: daysFromStart));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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,11 +1,10 @@
 | 
				
			|||||||
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/types/account.dart';
 | 
					import 'package:surface/types/account.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserProvider extends ChangeNotifier {
 | 
					class UserProvider extends ChangeNotifier {
 | 
				
			||||||
@@ -13,11 +12,11 @@ class UserProvider extends ChangeNotifier {
 | 
				
			|||||||
  SnAccount? user;
 | 
					  SnAccount? user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  late final SnNetworkProvider _sn;
 | 
					  late final SnNetworkProvider _sn;
 | 
				
			||||||
  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>();
 | 
					    _config = context.read<ConfigProvider>();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<String?> get atk async {
 | 
					  Future<String?> get atk async {
 | 
				
			||||||
@@ -26,14 +25,13 @@ 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) async {
 | 
				
			||||||
      if (value != null) {
 | 
					      if (value != null) {
 | 
				
			||||||
        log('Logged in as @${value.name}');
 | 
					        log('Logged in as @${value.name}');
 | 
				
			||||||
        _home.saveWidgetData('user', value.toJson());
 | 
					        log('Atk: ${await atk}');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -55,4 +53,11 @@ class UserProvider extends ChangeNotifier {
 | 
				
			|||||||
    user = null;
 | 
					    user = null;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setLanguage(String? value) {
 | 
				
			||||||
 | 
					    if (value == null) return;
 | 
				
			||||||
 | 
					    if (user == null) return;
 | 
				
			||||||
 | 
					    user = user!.copyWith(language: value);
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,8 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
  late final SnNetworkProvider _sn;
 | 
					  late final SnNetworkProvider _sn;
 | 
				
			||||||
  late final UserProvider _ua;
 | 
					  late final UserProvider _ua;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  StreamController<WebSocketPackage> stream = StreamController.broadcast();
 | 
					  StreamController<WebSocketPackage> pk = StreamController.broadcast();
 | 
				
			||||||
 | 
					  Stream<dynamic>? _wsStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  WebSocketProvider(BuildContext context) {
 | 
					  WebSocketProvider(BuildContext context) {
 | 
				
			||||||
    _sn = context.read<SnNetworkProvider>();
 | 
					    _sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
@@ -33,23 +34,33 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
    await connect();
 | 
					    await connect();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Completer<void>? _connectCompleter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> connect({noRetry = false}) async {
 | 
					  Future<void> connect({noRetry = false}) async {
 | 
				
			||||||
 | 
					    if (_connectCompleter != null) {
 | 
				
			||||||
 | 
					      await _connectCompleter!.future;
 | 
				
			||||||
 | 
					      _connectCompleter = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!_ua.isAuthorized) return;
 | 
					    if (!_ua.isAuthorized) return;
 | 
				
			||||||
    if (isConnected) {
 | 
					    if (isConnected || conn != null) {
 | 
				
			||||||
      disconnect();
 | 
					      disconnect();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final atk = await _sn.getFreshAtk();
 | 
					 | 
				
			||||||
    final uri = Uri.parse(
 | 
					 | 
				
			||||||
      '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    isBusy = true;
 | 
					 | 
				
			||||||
    notifyListeners();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      _connectCompleter = Completer<void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final atk = await _sn.getFreshAtk();
 | 
				
			||||||
 | 
					      final uri = Uri.parse(
 | 
				
			||||||
 | 
					        '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      isBusy = true;
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      conn = WebSocketChannel.connect(uri);
 | 
					      conn = WebSocketChannel.connect(uri);
 | 
				
			||||||
      await conn!.ready;
 | 
					      await conn!.ready;
 | 
				
			||||||
 | 
					      _wsStream = conn!.stream.asBroadcastStream();
 | 
				
			||||||
      listen();
 | 
					      listen();
 | 
				
			||||||
      log('[WebSocket] Connected to server!');
 | 
					      log('[WebSocket] Connected to server!');
 | 
				
			||||||
      isConnected = true;
 | 
					      isConnected = true;
 | 
				
			||||||
@@ -70,6 +81,8 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      isBusy = false;
 | 
					      isBusy = false;
 | 
				
			||||||
      notifyListeners();
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					      _connectCompleter!.complete();
 | 
				
			||||||
 | 
					      _connectCompleter = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,11 +96,12 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void listen() {
 | 
					  void listen() {
 | 
				
			||||||
    conn?.stream.listen(
 | 
					    if (_wsStream == null) return;
 | 
				
			||||||
 | 
					    _wsStream!.listen(
 | 
				
			||||||
      (event) {
 | 
					      (event) {
 | 
				
			||||||
        final packet = WebSocketPackage.fromJson(jsonDecode(event));
 | 
					        final packet = WebSocketPackage.fromJson(jsonDecode(event));
 | 
				
			||||||
        log('Websocket incoming message: ${packet.method} ${packet.message}');
 | 
					        log('Websocket incoming message: ${packet.method} ${packet.message}');
 | 
				
			||||||
        stream.sink.add(packet);
 | 
					        pk.sink.add(packet);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      onDone: () {
 | 
					      onDone: () {
 | 
				
			||||||
        isConnected = false;
 | 
					        isConnected = false;
 | 
				
			||||||
@@ -97,7 +111,7 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
      onError: (err) {
 | 
					      onError: (err) {
 | 
				
			||||||
        isConnected = false;
 | 
					        isConnected = false;
 | 
				
			||||||
        notifyListeners();
 | 
					        notifyListeners();
 | 
				
			||||||
        Future.delayed(const Duration(seconds: 11), () => connect());
 | 
					        Future.delayed(const Duration(seconds: 1), () => connect());
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,17 @@ class HomeWidgetProvider {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> widgetUpdateRandomPost() async {
 | 
				
			||||||
 | 
					  if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
 | 
				
			||||||
 | 
					  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",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										428
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:surface/screens/abuse_report.dart';
 | 
					import 'package:surface/screens/abuse_report.dart';
 | 
				
			||||||
import 'package:surface/screens/account.dart';
 | 
					import 'package:surface/screens/account.dart';
 | 
				
			||||||
import 'package:surface/screens/account/pfp.dart';
 | 
					import 'package:surface/screens/account/account_settings.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/account/factor_settings.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/account/profile_page.dart';
 | 
				
			||||||
import 'package:surface/screens/account/profile_edit.dart';
 | 
					import 'package:surface/screens/account/profile_edit.dart';
 | 
				
			||||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
 | 
					import 'package:surface/screens/account/publishers/publisher_edit.dart';
 | 
				
			||||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
 | 
					import 'package:surface/screens/account/publishers/publisher_new.dart';
 | 
				
			||||||
@@ -19,6 +21,8 @@ import 'package:surface/screens/chat/room.dart';
 | 
				
			|||||||
import 'package:surface/screens/explore.dart';
 | 
					import 'package:surface/screens/explore.dart';
 | 
				
			||||||
import 'package:surface/screens/friend.dart';
 | 
					import 'package:surface/screens/friend.dart';
 | 
				
			||||||
import 'package:surface/screens/home.dart';
 | 
					import 'package:surface/screens/home.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/news/news_detail.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/news/news_list.dart';
 | 
				
			||||||
import 'package:surface/screens/notification.dart';
 | 
					import 'package:surface/screens/notification.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_detail.dart';
 | 
					import 'package:surface/screens/post/post_detail.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_editor.dart';
 | 
					import 'package:surface/screens/post/post_editor.dart';
 | 
				
			||||||
@@ -27,289 +31,235 @@ import 'package:surface/screens/post/post_search.dart';
 | 
				
			|||||||
import 'package:surface/screens/realm.dart';
 | 
					import 'package:surface/screens/realm.dart';
 | 
				
			||||||
import 'package:surface/screens/realm/manage.dart';
 | 
					import 'package:surface/screens/realm/manage.dart';
 | 
				
			||||||
import 'package:surface/screens/realm/realm_detail.dart';
 | 
					import 'package:surface/screens/realm/realm_detail.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/realm/realm_discovery.dart';
 | 
				
			||||||
import 'package:surface/screens/settings.dart';
 | 
					import 'package:surface/screens/settings.dart';
 | 
				
			||||||
import 'package:surface/screens/sharing.dart';
 | 
					import 'package:surface/screens/sharing.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/wallet.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/about.dart';
 | 
					import 'package:surface/widgets/about.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Widget _fadeThroughTransition(
 | 
				
			||||||
 | 
					    BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
 | 
				
			||||||
 | 
					  return FadeThroughTransition(
 | 
				
			||||||
 | 
					    animation: animation,
 | 
				
			||||||
 | 
					    secondaryAnimation: secondaryAnimation,
 | 
				
			||||||
 | 
					    fillColor: Colors.transparent,
 | 
				
			||||||
 | 
					    child: child,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final _appRoutes = [
 | 
					final _appRoutes = [
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(
 | 
					    path: '/',
 | 
				
			||||||
      body: child,
 | 
					    name: 'home',
 | 
				
			||||||
      showAppBar: false,
 | 
					    builder: (context, state) => const HomeScreen(),
 | 
				
			||||||
    ),
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/posts',
 | 
				
			||||||
 | 
					    name: 'explore',
 | 
				
			||||||
 | 
					    builder: (context, state) => const ExploreScreen(),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/',
 | 
					        path: '/write/:mode',
 | 
				
			||||||
        name: 'home',
 | 
					        name: 'postEditor',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostEditorScreen(
 | 
				
			||||||
          child: const HomeScreen(),
 | 
					          mode: state.pathParameters['mode']!,
 | 
				
			||||||
 | 
					          postEditId: int.tryParse(
 | 
				
			||||||
 | 
					            state.uri.queryParameters['editing'] ?? '',
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          postReplyId: int.tryParse(
 | 
				
			||||||
 | 
					            state.uri.queryParameters['replying'] ?? '',
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          postRepostId: int.tryParse(
 | 
				
			||||||
 | 
					            state.uri.queryParameters['reposting'] ?? '',
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          extraProps: state.extra as PostEditorExtra?,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/posts',
 | 
					        path: '/search',
 | 
				
			||||||
        name: 'explore',
 | 
					        name: 'postSearch',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostSearchScreen(
 | 
				
			||||||
          child: const ExploreScreen(),
 | 
					          initialTags: state.uri.queryParameters['tags']?.split(','),
 | 
				
			||||||
        ),
 | 
					          initialCategories: state.uri.queryParameters['categories']?.split(','),
 | 
				
			||||||
        routes: [
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/write/:mode',
 | 
					 | 
				
			||||||
            name: 'postEditor',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: PostEditorScreen(
 | 
					 | 
				
			||||||
                mode: state.pathParameters['mode']!,
 | 
					 | 
				
			||||||
                postEditId: int.tryParse(
 | 
					 | 
				
			||||||
                  state.uri.queryParameters['editing'] ?? '',
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                postReplyId: int.tryParse(
 | 
					 | 
				
			||||||
                  state.uri.queryParameters['replying'] ?? '',
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                postRepostId: int.tryParse(
 | 
					 | 
				
			||||||
                  state.uri.queryParameters['reposting'] ?? '',
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                extraProps: state.extra as PostEditorExtraProps?,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/search',
 | 
					 | 
				
			||||||
            name: 'postSearch',
 | 
					 | 
				
			||||||
            builder: (context, state) => const AppBackground(
 | 
					 | 
				
			||||||
              child: PostSearchScreen(),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/publishers/:name',
 | 
					 | 
				
			||||||
            name: 'postPublisher',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: PostPublisherScreen(name: state.pathParameters['name']!),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:slug',
 | 
					 | 
				
			||||||
            name: 'postDetail',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: PostDetailScreen(
 | 
					 | 
				
			||||||
                slug: state.pathParameters['slug']!,
 | 
					 | 
				
			||||||
                preload: state.extra as SnPost?,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account',
 | 
					 | 
				
			||||||
        name: 'account',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					 | 
				
			||||||
          child: const AccountScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        routes: [],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/chat',
 | 
					 | 
				
			||||||
        name: 'chat',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					 | 
				
			||||||
          child: const ChatScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        routes: [
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:scope/:alias',
 | 
					 | 
				
			||||||
            name: 'chatRoom',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: ChatRoomScreen(
 | 
					 | 
				
			||||||
                scope: state.pathParameters['scope']!,
 | 
					 | 
				
			||||||
                alias: state.pathParameters['alias']!,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:scope/:alias/call',
 | 
					 | 
				
			||||||
            name: 'chatCallRoom',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: CallRoomScreen(
 | 
					 | 
				
			||||||
                scope: state.pathParameters['scope']!,
 | 
					 | 
				
			||||||
                alias: state.pathParameters['alias']!,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:scope/:alias/detail',
 | 
					 | 
				
			||||||
            name: 'channelDetail',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: ChannelDetailScreen(
 | 
					 | 
				
			||||||
                scope: state.pathParameters['scope']!,
 | 
					 | 
				
			||||||
                alias: state.pathParameters['alias']!,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/manage',
 | 
					 | 
				
			||||||
            name: 'chatManage',
 | 
					 | 
				
			||||||
            pageBuilder: (context, state) => CustomTransitionPage(
 | 
					 | 
				
			||||||
              child: ChatManageScreen(
 | 
					 | 
				
			||||||
                editingChannelAlias: state.uri.queryParameters['editing'],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
					 | 
				
			||||||
                return FadeThroughTransition(
 | 
					 | 
				
			||||||
                  animation: animation,
 | 
					 | 
				
			||||||
                  secondaryAnimation: secondaryAnimation,
 | 
					 | 
				
			||||||
                  fillColor: Colors.transparent,
 | 
					 | 
				
			||||||
                  child: AppBackground(
 | 
					 | 
				
			||||||
                    child: child,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:alias',
 | 
					 | 
				
			||||||
            name: 'realmDetail',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/realm',
 | 
					 | 
				
			||||||
        name: 'realm',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					 | 
				
			||||||
          child: const RealmScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        routes: [
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/manage',
 | 
					 | 
				
			||||||
            name: 'realmManage',
 | 
					 | 
				
			||||||
            pageBuilder: (context, state) => CustomTransitionPage(
 | 
					 | 
				
			||||||
              child: RealmManageScreen(
 | 
					 | 
				
			||||||
                editingRealmAlias: state.uri.queryParameters['editing'],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
					 | 
				
			||||||
                return FadeThroughTransition(
 | 
					 | 
				
			||||||
                  animation: animation,
 | 
					 | 
				
			||||||
                  secondaryAnimation: secondaryAnimation,
 | 
					 | 
				
			||||||
                  fillColor: Colors.transparent,
 | 
					 | 
				
			||||||
                  child: AppBackground(
 | 
					 | 
				
			||||||
                    child: child,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/album',
 | 
					 | 
				
			||||||
        name: 'album',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					 | 
				
			||||||
          child: const AlbumScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/friend',
 | 
					        path: '/publishers/:name',
 | 
				
			||||||
        name: 'friend',
 | 
					        name: 'postPublisher',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
 | 
				
			||||||
          child: const FriendScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/notification',
 | 
					        path: '/:slug',
 | 
				
			||||||
        name: 'notification',
 | 
					        name: 'postDetail',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostDetailScreen(
 | 
				
			||||||
          child: const NotificationScreen(),
 | 
					          slug: state.pathParameters['slug']!,
 | 
				
			||||||
 | 
					          preload: state.extra as SnPost?,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/wallet',
 | 
				
			||||||
 | 
					      name: 'accountWallet',
 | 
				
			||||||
 | 
					      builder: (context, state) => const WalletScreen(),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/settings',
 | 
				
			||||||
 | 
					      name: 'accountSettings',
 | 
				
			||||||
 | 
					      builder: (context, state) => AccountSettingsScreen(),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/settings/factors',
 | 
				
			||||||
 | 
					      name: 'factorSettings',
 | 
				
			||||||
 | 
					      builder: (context, state) => FactorSettingsScreen(),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/profile/edit',
 | 
				
			||||||
 | 
					      name: 'accountProfileEdit',
 | 
				
			||||||
 | 
					      builder: (context, state) => ProfileEditScreen(),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/publishers',
 | 
				
			||||||
 | 
					      name: 'accountPublishers',
 | 
				
			||||||
 | 
					      builder: (context, state) => PublisherScreen(),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/publishers/new',
 | 
				
			||||||
 | 
					      name: 'accountPublisherNew',
 | 
				
			||||||
 | 
					      builder: (context, state) => AccountPublisherNewScreen(),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/publishers/edit/:name',
 | 
				
			||||||
 | 
					      name: 'accountPublisherEdit',
 | 
				
			||||||
 | 
					      builder: (context, state) => AccountPublisherEditScreen(
 | 
				
			||||||
 | 
					        name: state.pathParameters['name']!,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    GoRoute(
 | 
				
			||||||
 | 
					      path: '/:name',
 | 
				
			||||||
 | 
					      name: 'accountProfilePage',
 | 
				
			||||||
 | 
					      pageBuilder: (context, state) => NoTransitionPage(
 | 
				
			||||||
 | 
					        child: UserScreen(name: state.pathParameters['name']!),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  ]),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/chat',
 | 
				
			||||||
 | 
					    name: 'chat',
 | 
				
			||||||
 | 
					    builder: (context, state) => const ChatScreen(),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/auth/login',
 | 
					        path: '/:scope/:alias',
 | 
				
			||||||
        name: 'authLogin',
 | 
					        name: 'chatRoom',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => ChatRoomScreen(
 | 
				
			||||||
          child: LoginScreen(),
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
 | 
					          extra: state.extra as ChatRoomScreenExtra?,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/auth/register',
 | 
					        path: '/:scope/:alias/call',
 | 
				
			||||||
        name: 'authRegister',
 | 
					        name: 'chatCallRoom',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => CallRoomScreen(
 | 
				
			||||||
          child: RegisterScreen(),
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/reports',
 | 
					        path: '/:scope/:alias/detail',
 | 
				
			||||||
        name: 'abuseReport',
 | 
					        name: 'channelDetail',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => ChannelDetailScreen(
 | 
				
			||||||
          child: AbuseReportScreen(),
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/account/profile/edit',
 | 
					        path: '/manage',
 | 
				
			||||||
        name: 'accountProfileEdit',
 | 
					        name: 'chatManage',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => ChatManageScreen(
 | 
				
			||||||
          child: ProfileEditScreen(),
 | 
					          editingChannelAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account/publishers',
 | 
					 | 
				
			||||||
        name: 'accountPublishers',
 | 
					 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					 | 
				
			||||||
          child: PublisherScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account/publishers/new',
 | 
					 | 
				
			||||||
        name: 'accountPublisherNew',
 | 
					 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					 | 
				
			||||||
          child: AccountPublisherNewScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account/publishers/edit/:name',
 | 
					 | 
				
			||||||
        name: 'accountPublisherEdit',
 | 
					 | 
				
			||||||
        builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
          child: AccountPublisherEditScreen(
 | 
					 | 
				
			||||||
            name: state.pathParameters['name']!,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  GoRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    path: '/account/:name',
 | 
					    path: '/realm',
 | 
				
			||||||
    name: 'accountProfilePage',
 | 
					    name: 'realm',
 | 
				
			||||||
    pageBuilder: (context, state) => NoTransitionPage(
 | 
					    pageBuilder: (context, state) => CustomTransitionPage(
 | 
				
			||||||
      child: UserScreen(name: state.pathParameters['name']!),
 | 
					      transitionsBuilder: _fadeThroughTransition,
 | 
				
			||||||
 | 
					      child: const RealmScreen(),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  ),
 | 
					 | 
				
			||||||
  ShellRoute(
 | 
					 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
					 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/settings',
 | 
					        path: '/manage',
 | 
				
			||||||
        name: 'settings',
 | 
					        name: 'realmManage',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => RealmManageScreen(
 | 
				
			||||||
          child: SettingsScreen(),
 | 
					          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
					      GoRoute(
 | 
				
			||||||
 | 
					        path: '/discovery',
 | 
				
			||||||
 | 
					        name: 'realmDiscovery',
 | 
				
			||||||
 | 
					        builder: (context, state) => const RealmDiscoveryScreen(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      GoRoute(
 | 
				
			||||||
 | 
					        path: '/:alias',
 | 
				
			||||||
 | 
					        name: 'realmDetail',
 | 
				
			||||||
 | 
					        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
					    GoRoute(
 | 
				
			||||||
    routes: [
 | 
					      path: '/:hash',
 | 
				
			||||||
      GoRoute(
 | 
					      name: 'newsDetail',
 | 
				
			||||||
        path: '/about',
 | 
					      builder: (context, state) => NewsDetailScreen(
 | 
				
			||||||
        name: 'about',
 | 
					        hash: state.pathParameters['hash']!,
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					 | 
				
			||||||
          child: AboutScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ),
 | 
				
			||||||
 | 
					  ]),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/album',
 | 
				
			||||||
 | 
					    name: 'album',
 | 
				
			||||||
 | 
					    builder: (context, state) => const AlbumScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/friend',
 | 
				
			||||||
 | 
					    name: 'friend',
 | 
				
			||||||
 | 
					    builder: (context, state) => const FriendScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/notification',
 | 
				
			||||||
 | 
					    name: 'notification',
 | 
				
			||||||
 | 
					    builder: (context, state) => const NotificationScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/auth/login',
 | 
				
			||||||
 | 
					    name: 'authLogin',
 | 
				
			||||||
 | 
					    builder: (context, state) => LoginScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/auth/register',
 | 
				
			||||||
 | 
					    name: 'authRegister',
 | 
				
			||||||
 | 
					    builder: (context, state) => RegisterScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/reports',
 | 
				
			||||||
 | 
					    name: 'abuseReport',
 | 
				
			||||||
 | 
					    builder: (context, state) => AbuseReportScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/settings',
 | 
				
			||||||
 | 
					    name: 'settings',
 | 
				
			||||||
 | 
					    builder: (context, state) => SettingsScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/about',
 | 
				
			||||||
 | 
					    name: 'about',
 | 
				
			||||||
 | 
					    builder: (context, state) => AboutScreen(),
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
 | 
				
			|||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../types/account.dart';
 | 
					import '../types/account.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAbuseReport').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: Column(
 | 
					      body: Column(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
@@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
 | 
				
			|||||||
          else
 | 
					          else
 | 
				
			||||||
            Expanded(
 | 
					            Expanded(
 | 
				
			||||||
              child: ListView.builder(
 | 
					              child: ListView.builder(
 | 
				
			||||||
 | 
					                padding: EdgeInsets.only(top: 8),
 | 
				
			||||||
                itemCount: _reports.length,
 | 
					                itemCount: _reports.length,
 | 
				
			||||||
                itemBuilder: (context, idx) {
 | 
					                itemBuilder: (context, idx) {
 | 
				
			||||||
                  return ListTile(
 | 
					                  return ListTile(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
@@ -12,6 +14,8 @@ import 'package:surface/providers/websocket.dart';
 | 
				
			|||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.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/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountScreen extends StatelessWidget {
 | 
					class AccountScreen extends StatelessWidget {
 | 
				
			||||||
  const AccountScreen({super.key});
 | 
					  const AccountScreen({super.key});
 | 
				
			||||||
@@ -19,11 +23,51 @@ class AccountScreen extends StatelessWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final ua = context.watch<UserProvider>();
 | 
					    final ua = context.watch<UserProvider>();
 | 
				
			||||||
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text("screenAccount").tr(),
 | 
					        title: Text(
 | 
				
			||||||
 | 
					          "screenAccount",
 | 
				
			||||||
 | 
					          style: TextStyle(
 | 
				
			||||||
 | 
					            color: Colors.white,
 | 
				
			||||||
 | 
					            shadows: [
 | 
				
			||||||
 | 
					              Shadow(
 | 
				
			||||||
 | 
					                offset: Offset(1, 1),
 | 
				
			||||||
 | 
					                blurRadius: 5.0,
 | 
				
			||||||
 | 
					                color: Color.fromARGB(255, 0, 0, 0),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ).tr(),
 | 
				
			||||||
 | 
					        flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
 | 
				
			||||||
 | 
					            ? Stack(
 | 
				
			||||||
 | 
					                fit: StackFit.expand,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
 | 
				
			||||||
 | 
					                  Positioned(
 | 
				
			||||||
 | 
					                    top: 0,
 | 
				
			||||||
 | 
					                    left: 0,
 | 
				
			||||||
 | 
					                    right: 0,
 | 
				
			||||||
 | 
					                    height: 56 + MediaQuery.of(context).padding.top,
 | 
				
			||||||
 | 
					                    child: ClipRect(
 | 
				
			||||||
 | 
					                      child: BackdropFilter(
 | 
				
			||||||
 | 
					                        filter: ImageFilter.blur(
 | 
				
			||||||
 | 
					                          sigmaX: 10,
 | 
				
			||||||
 | 
					                          sigmaY: 10,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        child: Container(
 | 
				
			||||||
 | 
					                          color: Colors.black.withOpacity(
 | 
				
			||||||
 | 
					                            clampDouble(10 * 0.1, 0, 0.5),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            : null,
 | 
				
			||||||
        actions: [
 | 
					        actions: [
 | 
				
			||||||
          IconButton(
 | 
					          IconButton(
 | 
				
			||||||
            icon: const Icon(Symbols.settings, fill: 1),
 | 
					            icon: const Icon(Symbols.settings, fill: 1),
 | 
				
			||||||
@@ -82,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
          }).padding(all: 20),
 | 
					          }).padding(all: 20),
 | 
				
			||||||
        ).padding(horizontal: 8, top: 16, bottom: 4),
 | 
					        ).padding(horizontal: 8, top: 16, bottom: 4),
 | 
				
			||||||
        ListTile(
 | 
					 | 
				
			||||||
          title: Text('accountProfileEdit').tr(),
 | 
					 | 
				
			||||||
          subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
					 | 
				
			||||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
					 | 
				
			||||||
          leading: const Icon(Symbols.contact_page),
 | 
					 | 
				
			||||||
          trailing: const Icon(Symbols.chevron_right),
 | 
					 | 
				
			||||||
          onTap: () {
 | 
					 | 
				
			||||||
            GoRouter.of(context).pushNamed('accountProfileEdit');
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text('accountPublishers').tr(),
 | 
					          title: Text('accountPublishers').tr(),
 | 
				
			||||||
          subtitle: Text('accountPublishersSubtitle').tr(),
 | 
					          subtitle: Text('accountPublishersSubtitle').tr(),
 | 
				
			||||||
@@ -112,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
            GoRouter.of(context).pushNamed('abuseReport');
 | 
					            GoRouter.of(context).pushNamed('abuseReport');
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        ListTile(
 | 
				
			||||||
 | 
					          title: Text('factorSettings').tr(),
 | 
				
			||||||
 | 
					          subtitle: Text('factorSettingsSubtitle').tr(),
 | 
				
			||||||
 | 
					          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
 | 
					          leading: const Icon(Symbols.lock),
 | 
				
			||||||
 | 
					          trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            GoRouter.of(context).pushNamed('factorSettings');
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        ListTile(
 | 
				
			||||||
 | 
					          title: Text('accountWallet').tr(),
 | 
				
			||||||
 | 
					          subtitle: Text('accountWalletSubtitle').tr(),
 | 
				
			||||||
 | 
					          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
 | 
					          leading: const Icon(Symbols.wallet),
 | 
				
			||||||
 | 
					          trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            GoRouter.of(context).pushNamed('accountWallet');
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        ListTile(
 | 
				
			||||||
 | 
					          title: Text('accountSettings').tr(),
 | 
				
			||||||
 | 
					          subtitle: Text('accountSettingsSubtitle').tr(),
 | 
				
			||||||
 | 
					          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
 | 
					          leading: const Icon(Symbols.manage_accounts),
 | 
				
			||||||
 | 
					          trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            GoRouter.of(context).pushNamed('accountSettings');
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text('accountLogout').tr(),
 | 
					          title: Text('accountLogout').tr(),
 | 
				
			||||||
          subtitle: Text('accountLogoutSubtitle').tr(),
 | 
					          subtitle: Text('accountLogoutSubtitle').tr(),
 | 
				
			||||||
@@ -133,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
            await Hive.initFlutter();
 | 
					            await Hive.initFlutter();
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        ListTile(
 | 
					 | 
				
			||||||
          title: Text('accountDeletion'.tr()),
 | 
					 | 
				
			||||||
          subtitle: Text('accountDeletionActionDescription'.tr()),
 | 
					 | 
				
			||||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
					 | 
				
			||||||
          leading: const Icon(Symbols.person_cancel),
 | 
					 | 
				
			||||||
          trailing: const Icon(Symbols.chevron_right),
 | 
					 | 
				
			||||||
          onTap: () {
 | 
					 | 
				
			||||||
            context
 | 
					 | 
				
			||||||
                .showConfirmDialog(
 | 
					 | 
				
			||||||
              'accountDeletion'.tr(),
 | 
					 | 
				
			||||||
              'accountDeletionDescription'.tr(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
                .then((value) {
 | 
					 | 
				
			||||||
              if (!value || !context.mounted) return;
 | 
					 | 
				
			||||||
              final sn = context.read<SnNetworkProvider>();
 | 
					 | 
				
			||||||
              sn.client.post('/cgi/id/users/me/deletion').then((value) {
 | 
					 | 
				
			||||||
                if (context.mounted) {
 | 
					 | 
				
			||||||
                  context.showSnackbar('accountDeletionSubmitted'.tr());
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }).catchError((err) {
 | 
					 | 
				
			||||||
                if (context.mounted) {
 | 
					 | 
				
			||||||
                  context.showErrorDialog(err);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              });
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										126
									
								
								lib/screens/account/account_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:dropdown_button2/dropdown_button2.dart';
 | 
				
			||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					import 'package:intl/locale.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountSettingsScreen extends StatelessWidget {
 | 
				
			||||||
 | 
					  const AccountSettingsScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _setAccountLanguage(BuildContext context, Locale? value) async {
 | 
				
			||||||
 | 
					    if (value == null) return;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					      await sn.client.put('/cgi/id/users/me/language', data: {
 | 
				
			||||||
 | 
					        'language': value.toString(),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					      context.showSnackbar('accountSettingsApplied'.tr());
 | 
				
			||||||
 | 
					      await ua.refreshUser();
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final ua = context.watch<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAccountSettings').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            ListTile(
 | 
				
			||||||
 | 
					              title: Text('settingsAccountLanguage').tr(),
 | 
				
			||||||
 | 
					              subtitle: Text('settingsAccountLanguageDescription').tr(),
 | 
				
			||||||
 | 
					              contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
				
			||||||
 | 
					              leading: const Icon(Symbols.translate),
 | 
				
			||||||
 | 
					              trailing: DropdownButtonHideUnderline(
 | 
				
			||||||
 | 
					                child: DropdownButton2<Locale?>(
 | 
				
			||||||
 | 
					                  isExpanded: true,
 | 
				
			||||||
 | 
					                  items: [
 | 
				
			||||||
 | 
					                    ...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
 | 
				
			||||||
 | 
					                      return DropdownMenuItem<Locale?>(
 | 
				
			||||||
 | 
					                        value: Locale.parse(ele.toString()),
 | 
				
			||||||
 | 
					                        child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                  value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
 | 
				
			||||||
 | 
					                  onChanged: (Locale? value) {
 | 
				
			||||||
 | 
					                    if (value == null) return;
 | 
				
			||||||
 | 
					                    _setAccountLanguage(context, value);
 | 
				
			||||||
 | 
					                    ua.setLanguage(value.toString());
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                  buttonStyleData: const ButtonStyleData(
 | 
				
			||||||
 | 
					                    padding: EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                      horizontal: 16,
 | 
				
			||||||
 | 
					                      vertical: 5,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    height: 40,
 | 
				
			||||||
 | 
					                    width: 160,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  menuItemStyleData: const MenuItemStyleData(
 | 
				
			||||||
 | 
					                    height: 40,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            ListTile(
 | 
				
			||||||
 | 
					              title: Text('accountProfileEdit').tr(),
 | 
				
			||||||
 | 
					              subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
				
			||||||
 | 
					              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
 | 
					              leading: const Icon(Symbols.contact_page),
 | 
				
			||||||
 | 
					              trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					                GoRouter.of(context).pushNamed('accountProfileEdit');
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            ListTile(
 | 
				
			||||||
 | 
					              title: Text('accountDeletion'.tr()),
 | 
				
			||||||
 | 
					              subtitle: Text('accountDeletionActionDescription'.tr()),
 | 
				
			||||||
 | 
					              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
 | 
					              leading: const Icon(Symbols.person_cancel),
 | 
				
			||||||
 | 
					              trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					                context
 | 
				
			||||||
 | 
					                    .showConfirmDialog(
 | 
				
			||||||
 | 
					                  'accountDeletion'.tr(),
 | 
				
			||||||
 | 
					                  'accountDeletionDescription'.tr(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                    .then((value) {
 | 
				
			||||||
 | 
					                  if (!value || !context.mounted) return;
 | 
				
			||||||
 | 
					                  final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					                  sn.client.post('/cgi/id/users/me/deletion').then((value) {
 | 
				
			||||||
 | 
					                    if (context.mounted) {
 | 
				
			||||||
 | 
					                      context.showSnackbar('accountDeletionSubmitted'.tr());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  }).catchError((err) {
 | 
				
			||||||
 | 
					                    if (context.mounted) {
 | 
				
			||||||
 | 
					                      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										294
									
								
								lib/screens/account/factor_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,294 @@
 | 
				
			|||||||
 | 
					import 'package:dropdown_button2/dropdown_button2.dart';
 | 
				
			||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:qr_flutter/qr_flutter.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/auth.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final Map<int, (String, String, IconData)> kFactorTypes = {
 | 
				
			||||||
 | 
					  0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
 | 
				
			||||||
 | 
					  1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
 | 
				
			||||||
 | 
					  2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
 | 
				
			||||||
 | 
					  3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FactorSettingsScreen extends StatefulWidget {
 | 
				
			||||||
 | 
					  const FactorSettingsScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
 | 
				
			||||||
 | 
					  bool _isBusy = false;
 | 
				
			||||||
 | 
					  List<SnAuthFactor>? _factors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _fetchFactors() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      final resp = await sn.client.get('/cgi/id/users/me/factors');
 | 
				
			||||||
 | 
					      _factors = List<SnAuthFactor>.from(
 | 
				
			||||||
 | 
					        resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _fetchFactors();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenFactorSettings').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: Column(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          LoadingIndicator(
 | 
				
			||||||
 | 
					            isActive: _isBusy,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          ListTile(
 | 
				
			||||||
 | 
					            title: Text('authFactorAdd').tr(),
 | 
				
			||||||
 | 
					            subtitle: Text('authFactorAddSubtitle').tr(),
 | 
				
			||||||
 | 
					            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
 | 
					            leading: const Icon(Symbols.add),
 | 
				
			||||||
 | 
					            trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					            onTap: () {
 | 
				
			||||||
 | 
					              showDialog(
 | 
				
			||||||
 | 
					                context: context,
 | 
				
			||||||
 | 
					                builder: (context) => _FactorNewDialog(
 | 
				
			||||||
 | 
					                  currentlyHave: _factors!,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ).then((val) {
 | 
				
			||||||
 | 
					                if (val == true) _fetchFactors();
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          const Divider(height: 1),
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
 | 
					              context: context,
 | 
				
			||||||
 | 
					              removeTop: true,
 | 
				
			||||||
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
 | 
					                onRefresh: _fetchFactors,
 | 
				
			||||||
 | 
					                child: ListView.builder(
 | 
				
			||||||
 | 
					                  itemCount: _factors?.length ?? 0,
 | 
				
			||||||
 | 
					                  itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					                    final ele = _factors![idx];
 | 
				
			||||||
 | 
					                    return ListTile(
 | 
				
			||||||
 | 
					                      title: Text(kFactorTypes[ele.type]!.$1).tr(),
 | 
				
			||||||
 | 
					                      subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
 | 
				
			||||||
 | 
					                      contentPadding: const EdgeInsets.only(left: 24, right: 12),
 | 
				
			||||||
 | 
					                      leading: Icon(kFactorTypes[ele.type]!.$3),
 | 
				
			||||||
 | 
					                      trailing: IconButton(
 | 
				
			||||||
 | 
					                        icon: const Icon(Symbols.close),
 | 
				
			||||||
 | 
					                        onPressed: ele.type > 0
 | 
				
			||||||
 | 
					                            ? () {
 | 
				
			||||||
 | 
					                                context
 | 
				
			||||||
 | 
					                                    .showConfirmDialog(
 | 
				
			||||||
 | 
					                                  'authFactorDelete'.tr(),
 | 
				
			||||||
 | 
					                                  'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                    .then((val) async {
 | 
				
			||||||
 | 
					                                  if (!val) return;
 | 
				
			||||||
 | 
					                                  try {
 | 
				
			||||||
 | 
					                                    if (!context.mounted) return;
 | 
				
			||||||
 | 
					                                    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					                                    await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
 | 
				
			||||||
 | 
					                                    _fetchFactors();
 | 
				
			||||||
 | 
					                                  } catch (err) {
 | 
				
			||||||
 | 
					                                    if (!context.mounted) return;
 | 
				
			||||||
 | 
					                                    context.showErrorDialog(err);
 | 
				
			||||||
 | 
					                                  }
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                              }
 | 
				
			||||||
 | 
					                            : null,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _FactorNewDialog extends StatefulWidget {
 | 
				
			||||||
 | 
					  final List<SnAuthFactor> currentlyHave;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _FactorNewDialog({required this.currentlyHave});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<_FactorNewDialog> createState() => _FactorNewDialogState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _FactorNewDialogState extends State<_FactorNewDialog> {
 | 
				
			||||||
 | 
					  int? _factorType;
 | 
				
			||||||
 | 
					  bool _isBusy = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _submit() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
 | 
				
			||||||
 | 
					        'type': _factorType,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      final factor = SnAuthFactor.fromJson(resp.data);
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      if (factor.type == 2) {
 | 
				
			||||||
 | 
					        await showModalBottomSheet(
 | 
				
			||||||
 | 
					          context: context,
 | 
				
			||||||
 | 
					          builder: (context) => _FactorTotpFactorDialog(factor: factor),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      Navigator.of(context).pop(true);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return AlertDialog(
 | 
				
			||||||
 | 
					      title: Text('authFactorAdd').tr(),
 | 
				
			||||||
 | 
					      content: Column(
 | 
				
			||||||
 | 
					        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          DropdownButtonHideUnderline(
 | 
				
			||||||
 | 
					            child: DropdownButton2<int>(
 | 
				
			||||||
 | 
					              hint: Text(
 | 
				
			||||||
 | 
					                'Select Item',
 | 
				
			||||||
 | 
					                style: TextStyle(
 | 
				
			||||||
 | 
					                  fontSize: 14,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              value: _factorType,
 | 
				
			||||||
 | 
					              items: kFactorTypes.entries.map(
 | 
				
			||||||
 | 
					                (ele) {
 | 
				
			||||||
 | 
					                  final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
 | 
				
			||||||
 | 
					                  return DropdownMenuItem<int>(
 | 
				
			||||||
 | 
					                    enabled: !contains,
 | 
				
			||||||
 | 
					                    value: ele.key,
 | 
				
			||||||
 | 
					                    child: Text(
 | 
				
			||||||
 | 
					                      ele.value.$1.tr(),
 | 
				
			||||||
 | 
					                      style: const TextStyle(
 | 
				
			||||||
 | 
					                        fontSize: 14,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ).opacity(contains ? 0.75 : 1),
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ).toList(),
 | 
				
			||||||
 | 
					              onChanged: (val) => setState(() {
 | 
				
			||||||
 | 
					                _factorType = val;
 | 
				
			||||||
 | 
					              }),
 | 
				
			||||||
 | 
					              buttonStyleData: ButtonStyleData(
 | 
				
			||||||
 | 
					                height: 50,
 | 
				
			||||||
 | 
					                padding: const EdgeInsets.only(left: 14, right: 14),
 | 
				
			||||||
 | 
					                decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.circular(14),
 | 
				
			||||||
 | 
					                  border: Border.all(
 | 
				
			||||||
 | 
					                    color: Theme.of(context).dividerColor,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        TextButton(
 | 
				
			||||||
 | 
					          onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
 | 
				
			||||||
 | 
					          child: Text('dialogCancel').tr(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        TextButton(
 | 
				
			||||||
 | 
					          onPressed: _isBusy ? null : () => _submit(),
 | 
				
			||||||
 | 
					          child: Text('dialogConfirm').tr(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _FactorTotpFactorDialog extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnAuthFactor factor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _FactorTotpFactorDialog({required this.factor});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Center(
 | 
				
			||||||
 | 
					      child: Column(
 | 
				
			||||||
 | 
					        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Center(
 | 
				
			||||||
 | 
					            child: Text(
 | 
				
			||||||
 | 
					              'totpPostSetup',
 | 
				
			||||||
 | 
					              textAlign: TextAlign.center,
 | 
				
			||||||
 | 
					              style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					            ).tr().width(280),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          const Gap(4),
 | 
				
			||||||
 | 
					          Center(
 | 
				
			||||||
 | 
					            child: Text(
 | 
				
			||||||
 | 
					              'totpPostSetupDescription',
 | 
				
			||||||
 | 
					              textAlign: TextAlign.center,
 | 
				
			||||||
 | 
					              style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
 | 
					            ).tr().width(280),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          const Gap(16),
 | 
				
			||||||
 | 
					          QrImageView(
 | 
				
			||||||
 | 
					            padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					            data: factor.config!['url'],
 | 
				
			||||||
 | 
					            errorCorrectionLevel: QrErrorCorrectLevel.H,
 | 
				
			||||||
 | 
					            version: QrVersions.auto,
 | 
				
			||||||
 | 
					            size: 160,
 | 
				
			||||||
 | 
					            gapless: true,
 | 
				
			||||||
 | 
					            eyeStyle: QrEyeStyle(
 | 
				
			||||||
 | 
					              eyeShape: QrEyeShape.circle,
 | 
				
			||||||
 | 
					              color: Theme.of(context).colorScheme.onSurface,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            dataModuleStyle: QrDataModuleStyle(
 | 
				
			||||||
 | 
					              dataModuleShape: QrDataModuleShape.square,
 | 
				
			||||||
 | 
					              color: Theme.of(context).colorScheme.onSurface,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          const Gap(16),
 | 
				
			||||||
 | 
					          Center(
 | 
				
			||||||
 | 
					            child: Text(
 | 
				
			||||||
 | 
					              'totpNeverShare',
 | 
				
			||||||
 | 
					              textAlign: TextAlign.center,
 | 
				
			||||||
 | 
					              style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
 | 
					            ).tr().bold().width(280),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.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';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProfileEditScreen extends StatefulWidget {
 | 
					class ProfileEditScreen extends StatefulWidget {
 | 
				
			||||||
@@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
            onDateTimeChanged: (DateTime newDate) {
 | 
					            onDateTimeChanged: (DateTime newDate) {
 | 
				
			||||||
              setState(() {
 | 
					              setState(() {
 | 
				
			||||||
                _birthday = newDate;
 | 
					                _birthday = newDate;
 | 
				
			||||||
                _birthdayController.text =
 | 
					                _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
 | 
				
			||||||
                    DateFormat(_kDateFormat).format(_birthday!);
 | 
					 | 
				
			||||||
              });
 | 
					              });
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
    if (image == null) return;
 | 
					    if (image == null) return;
 | 
				
			||||||
    if (!mounted) return;
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final ImageProvider imageProvider =
 | 
					    final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
				
			||||||
        kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
					    final aspectRatios =
 | 
				
			||||||
    final aspectRatios = place == 'banner'
 | 
					        place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
 | 
				
			||||||
        ? [CropAspectRatio(width: 16, height: 7)]
 | 
					 | 
				
			||||||
        : [CropAspectRatio(width: 1, height: 1)];
 | 
					 | 
				
			||||||
    final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
					    final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
				
			||||||
        ? await showCupertinoImageCropper(
 | 
					        ? await showCupertinoImageCropper(
 | 
				
			||||||
            // ignore: use_build_context_synchronously
 | 
					            // ignore: use_build_context_synchronously
 | 
				
			||||||
@@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final rawBytes =
 | 
					    final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
 | 
				
			||||||
        (await result.uiImage.toByteData(format: ImageByteFormat.png))!
 | 
					 | 
				
			||||||
            .buffer
 | 
					 | 
				
			||||||
            .asUint8List();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final attachment = await attach.directUploadOne(
 | 
					      final attachment = await attach.directUploadOne(
 | 
				
			||||||
@@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SingleChildScrollView(
 | 
					    return AppScaffold(
 | 
				
			||||||
      child: Column(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
        children: [
 | 
					        title: Text('screenAccountProfileEdit').tr(),
 | 
				
			||||||
          LoadingIndicator(isActive: _isBusy),
 | 
					      ),
 | 
				
			||||||
          const Gap(24),
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
          Stack(
 | 
					        child: Column(
 | 
				
			||||||
            clipBehavior: Clip.none,
 | 
					          crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
            children: [
 | 
					          children: [
 | 
				
			||||||
              Material(
 | 
					            LoadingIndicator(isActive: _isBusy),
 | 
				
			||||||
                elevation: 0,
 | 
					            const Gap(24),
 | 
				
			||||||
                child: InkWell(
 | 
					            Stack(
 | 
				
			||||||
                  child: ClipRRect(
 | 
					              clipBehavior: Clip.none,
 | 
				
			||||||
                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					              children: [
 | 
				
			||||||
                    child: AspectRatio(
 | 
					                Material(
 | 
				
			||||||
                      aspectRatio: 16 / 9,
 | 
					                  elevation: 0,
 | 
				
			||||||
                      child: Container(
 | 
					                  child: InkWell(
 | 
				
			||||||
                        color:
 | 
					                    child: ClipRRect(
 | 
				
			||||||
                            Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
					                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
                        child: _banner != null
 | 
					                      child: AspectRatio(
 | 
				
			||||||
                            ? AutoResizeUniversalImage(
 | 
					                        aspectRatio: 16 / 9,
 | 
				
			||||||
                                sn.getAttachmentUrl(_banner!),
 | 
					                        child: Container(
 | 
				
			||||||
                                fit: BoxFit.cover,
 | 
					                          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
                              )
 | 
					                          child: _banner != null
 | 
				
			||||||
                            : const SizedBox.shrink(),
 | 
					                              ? AutoResizeUniversalImage(
 | 
				
			||||||
 | 
					                                  sn.getAttachmentUrl(_banner!),
 | 
				
			||||||
 | 
					                                  fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                              : const SizedBox.shrink(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  onTap: () {
 | 
					 | 
				
			||||||
                    _updateImage('banner');
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              Positioned(
 | 
					 | 
				
			||||||
                bottom: -28,
 | 
					 | 
				
			||||||
                left: 16,
 | 
					 | 
				
			||||||
                child: Material(
 | 
					 | 
				
			||||||
                  elevation: 2,
 | 
					 | 
				
			||||||
                  borderRadius: const BorderRadius.all(Radius.circular(40)),
 | 
					 | 
				
			||||||
                  child: InkWell(
 | 
					 | 
				
			||||||
                    child: AccountImage(content: _avatar, radius: 40),
 | 
					 | 
				
			||||||
                    onTap: () {
 | 
					                    onTap: () {
 | 
				
			||||||
                      _updateImage('avatar');
 | 
					                      _updateImage('banner');
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					                Positioned(
 | 
				
			||||||
            ],
 | 
					                  bottom: -28,
 | 
				
			||||||
          ).padding(horizontal: padding),
 | 
					                  left: 16,
 | 
				
			||||||
          const Gap(8 + 28),
 | 
					                  child: Material(
 | 
				
			||||||
          Column(
 | 
					                    elevation: 2,
 | 
				
			||||||
            children: [
 | 
					                    borderRadius: const BorderRadius.all(Radius.circular(40)),
 | 
				
			||||||
              TextField(
 | 
					                    child: InkWell(
 | 
				
			||||||
                readOnly: true,
 | 
					                      child: AccountImage(content: _avatar, radius: 40),
 | 
				
			||||||
                controller: _usernameController,
 | 
					                      onTap: () {
 | 
				
			||||||
                decoration: InputDecoration(
 | 
					                        _updateImage('avatar');
 | 
				
			||||||
                  border: const UnderlineInputBorder(),
 | 
					                      },
 | 
				
			||||||
                  labelText: 'fieldUsername'.tr(),
 | 
					 | 
				
			||||||
                  helperText: 'fieldUsernameCannotEditHint'.tr(),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Gap(4),
 | 
					 | 
				
			||||||
              TextField(
 | 
					 | 
				
			||||||
                controller: _nicknameController,
 | 
					 | 
				
			||||||
                decoration: InputDecoration(
 | 
					 | 
				
			||||||
                  border: const UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                  labelText: 'fieldNickname'.tr(),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Gap(4),
 | 
					 | 
				
			||||||
              Row(
 | 
					 | 
				
			||||||
                children: [
 | 
					 | 
				
			||||||
                  Flexible(
 | 
					 | 
				
			||||||
                    flex: 1,
 | 
					 | 
				
			||||||
                    child: TextField(
 | 
					 | 
				
			||||||
                      controller: _firstNameController,
 | 
					 | 
				
			||||||
                      decoration: InputDecoration(
 | 
					 | 
				
			||||||
                        border: const UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                        labelText: 'fieldFirstName'.tr(),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const Gap(8),
 | 
					                ),
 | 
				
			||||||
                  Flexible(
 | 
					              ],
 | 
				
			||||||
                    flex: 1,
 | 
					            ).padding(horizontal: padding),
 | 
				
			||||||
                    child: TextField(
 | 
					            const Gap(8 + 28),
 | 
				
			||||||
                      controller: _lastNameController,
 | 
					            Column(
 | 
				
			||||||
                      decoration: InputDecoration(
 | 
					              children: [
 | 
				
			||||||
                        border: const UnderlineInputBorder(),
 | 
					                TextField(
 | 
				
			||||||
                        labelText: 'fieldLastName'.tr(),
 | 
					                  readOnly: true,
 | 
				
			||||||
 | 
					                  controller: _usernameController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                    labelText: 'fieldUsername'.tr(),
 | 
				
			||||||
 | 
					                    helperText: 'fieldUsernameCannotEditHint'.tr(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const Gap(4),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: _nicknameController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                    labelText: 'fieldNickname'.tr(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const Gap(4),
 | 
				
			||||||
 | 
					                Row(
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    Flexible(
 | 
				
			||||||
 | 
					                      flex: 1,
 | 
				
			||||||
 | 
					                      child: TextField(
 | 
				
			||||||
 | 
					                        controller: _firstNameController,
 | 
				
			||||||
 | 
					                        decoration: InputDecoration(
 | 
				
			||||||
 | 
					                          border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                          labelText: 'fieldFirstName'.tr(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const Gap(8),
 | 
				
			||||||
 | 
					                    Flexible(
 | 
				
			||||||
 | 
					                      flex: 1,
 | 
				
			||||||
 | 
					                      child: TextField(
 | 
				
			||||||
 | 
					                        controller: _lastNameController,
 | 
				
			||||||
 | 
					                        decoration: InputDecoration(
 | 
				
			||||||
 | 
					                          border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                          labelText: 'fieldLastName'.tr(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const Gap(4),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: _descriptionController,
 | 
				
			||||||
 | 
					                  keyboardType: TextInputType.multiline,
 | 
				
			||||||
 | 
					                  maxLines: null,
 | 
				
			||||||
 | 
					                  minLines: 3,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                    labelText: 'fieldDescription'.tr(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Gap(4),
 | 
					 | 
				
			||||||
              TextField(
 | 
					 | 
				
			||||||
                controller: _descriptionController,
 | 
					 | 
				
			||||||
                keyboardType: TextInputType.multiline,
 | 
					 | 
				
			||||||
                maxLines: null,
 | 
					 | 
				
			||||||
                minLines: 3,
 | 
					 | 
				
			||||||
                decoration: InputDecoration(
 | 
					 | 
				
			||||||
                  border: const UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                  labelText: 'fieldDescription'.tr(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					                const Gap(4),
 | 
				
			||||||
              const Gap(4),
 | 
					                TextField(
 | 
				
			||||||
              TextField(
 | 
					                  controller: _birthdayController,
 | 
				
			||||||
                controller: _birthdayController,
 | 
					                  readOnly: true,
 | 
				
			||||||
                readOnly: true,
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
                decoration: InputDecoration(
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                  border: const UnderlineInputBorder(),
 | 
					                    labelText: 'fieldBirthday'.tr(),
 | 
				
			||||||
                  labelText: 'fieldBirthday'.tr(),
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTap: () => _selectBirthday(),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                onTap: () => _selectBirthday(),
 | 
					              ],
 | 
				
			||||||
              ),
 | 
					            ).padding(horizontal: padding + 8),
 | 
				
			||||||
            ],
 | 
					            const Gap(12),
 | 
				
			||||||
          ).padding(horizontal: padding + 8),
 | 
					            Row(
 | 
				
			||||||
          const Gap(12),
 | 
					              mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
          Row(
 | 
					              children: [
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.end,
 | 
					                ElevatedButton.icon(
 | 
				
			||||||
            children: [
 | 
					                  onPressed: _isBusy ? null : _updateUserInfo,
 | 
				
			||||||
              ElevatedButton.icon(
 | 
					                  icon: const Icon(Symbols.save),
 | 
				
			||||||
                onPressed: _isBusy ? null : _updateUserInfo,
 | 
					                  label: Text('apply').tr(),
 | 
				
			||||||
                icon: const Icon(Symbols.save),
 | 
					                ),
 | 
				
			||||||
                label: Text('apply').tr(),
 | 
					              ],
 | 
				
			||||||
              ),
 | 
					            ).padding(horizontal: padding),
 | 
				
			||||||
            ],
 | 
					          ],
 | 
				
			||||||
          ).padding(horizontal: padding),
 | 
					        ),
 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
@@ -225,68 +241,76 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
 | 
				
			|||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      backgroundColor: Colors.transparent,
 | 
				
			||||||
      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 +454,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 +462,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 +476,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 +486,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 +513,33 @@ 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();
 | 
				
			||||||
 | 
					                if (snapshot.data!.length <= 1) {
 | 
				
			||||||
 | 
					                  return Text(
 | 
				
			||||||
 | 
					                    'accountCheckInNoRecords',
 | 
				
			||||||
 | 
					                    textAlign: TextAlign.center,
 | 
				
			||||||
 | 
					                  ).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                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,
 | 
				
			||||||
@@ -521,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
 | 
				
			|||||||
                subtitle: Text('@${ele.name}'),
 | 
					                subtitle: Text('@${ele.name}'),
 | 
				
			||||||
                trailing: const Icon(Symbols.chevron_right),
 | 
					                trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  GoRouter.of(context).pushNamed(
 | 
					                  GoRouter.of(context).goNamed(
 | 
				
			||||||
                    'postPublisher',
 | 
					                    'postPublisher',
 | 
				
			||||||
                    pathParameters: {'name': ele.name},
 | 
					                    pathParameters: {'name': ele.name},
 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
@@ -534,3 +608,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),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -18,19 +18,19 @@ 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';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountPublisherEditScreen extends StatefulWidget {
 | 
					class AccountPublisherEditScreen extends StatefulWidget {
 | 
				
			||||||
  final String name;
 | 
					  final String name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AccountPublisherEditScreen({super.key, required this.name});
 | 
					  const AccountPublisherEditScreen({super.key, required this.name});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<AccountPublisherEditScreen> createState() =>
 | 
					  State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState();
 | 
				
			||||||
      _AccountPublisherEditScreenState();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _AccountPublisherEditScreenState
 | 
					class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> {
 | 
				
			||||||
    extends State<AccountPublisherEditScreen> {
 | 
					 | 
				
			||||||
  bool _isBusy = false;
 | 
					  bool _isBusy = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SnPublisher? _publisher;
 | 
					  SnPublisher? _publisher;
 | 
				
			||||||
@@ -54,7 +54,7 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
      _publisher = SnPublisher.fromJson(resp.data);
 | 
					      _publisher = SnPublisher.fromJson(resp.data);
 | 
				
			||||||
      _syncWidget();
 | 
					      _syncWidget();
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -75,9 +75,9 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
        'name': _nameController.text,
 | 
					        'name': _nameController.text,
 | 
				
			||||||
        'description': _descriptionController.text,
 | 
					        'description': _descriptionController.text,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      Navigator.pop(context, true);
 | 
					      if (mounted) Navigator.pop(context, true);
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					      if(mounted) context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -108,11 +108,9 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
    if (image == null) return;
 | 
					    if (image == null) return;
 | 
				
			||||||
    if (!mounted) return;
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final ImageProvider imageProvider =
 | 
					    final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
				
			||||||
        kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
					    final aspectRatios =
 | 
				
			||||||
    final aspectRatios = place == 'banner'
 | 
					        place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
 | 
				
			||||||
        ? [CropAspectRatio(width: 16, height: 7)]
 | 
					 | 
				
			||||||
        : [CropAspectRatio(width: 1, height: 1)];
 | 
					 | 
				
			||||||
    final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
					    final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
				
			||||||
        ? await showCupertinoImageCropper(
 | 
					        ? await showCupertinoImageCropper(
 | 
				
			||||||
            // ignore: use_build_context_synchronously
 | 
					            // ignore: use_build_context_synchronously
 | 
				
			||||||
@@ -134,10 +132,7 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final rawBytes =
 | 
					    final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
 | 
				
			||||||
        (await result.uiImage.toByteData(format: ImageByteFormat.png))!
 | 
					 | 
				
			||||||
            .buffer
 | 
					 | 
				
			||||||
            .asUint8List();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final attachment = await attach.directUploadOne(
 | 
					      final attachment = await attach.directUploadOne(
 | 
				
			||||||
@@ -182,7 +177,11 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAccountPublisherEdit').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
@@ -199,9 +198,7 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
                      child: AspectRatio(
 | 
					                      child: AspectRatio(
 | 
				
			||||||
                        aspectRatio: 16 / 9,
 | 
					                        aspectRatio: 16 / 9,
 | 
				
			||||||
                        child: Container(
 | 
					                        child: Container(
 | 
				
			||||||
                          color: Theme.of(context)
 | 
					                          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
                              .colorScheme
 | 
					 | 
				
			||||||
                              .surfaceContainerHigh,
 | 
					 | 
				
			||||||
                          child: _banner != null
 | 
					                          child: _banner != null
 | 
				
			||||||
                              ? AutoResizeUniversalImage(
 | 
					                              ? AutoResizeUniversalImage(
 | 
				
			||||||
                                  sn.getAttachmentUrl(_banner!),
 | 
					                                  sn.getAttachmentUrl(_banner!),
 | 
				
			||||||
@@ -240,8 +237,7 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
                labelText: 'fieldUsername'.tr(),
 | 
					                labelText: 'fieldUsername'.tr(),
 | 
				
			||||||
                helperText: 'fieldUsernameCannotEditHint'.tr(),
 | 
					                helperText: 'fieldUsernameCannotEditHint'.tr(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onTapOutside: (_) =>
 | 
					              onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            const Gap(4),
 | 
					            const Gap(4),
 | 
				
			||||||
            TextField(
 | 
					            TextField(
 | 
				
			||||||
@@ -249,8 +245,7 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
              decoration: InputDecoration(
 | 
					              decoration: InputDecoration(
 | 
				
			||||||
                labelText: 'fieldNickname'.tr(),
 | 
					                labelText: 'fieldNickname'.tr(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onTapOutside: (_) =>
 | 
					              onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            const Gap(4),
 | 
					            const Gap(4),
 | 
				
			||||||
            TextField(
 | 
					            TextField(
 | 
				
			||||||
@@ -260,8 +255,7 @@ class _AccountPublisherEditScreenState
 | 
				
			|||||||
              decoration: InputDecoration(
 | 
					              decoration: InputDecoration(
 | 
				
			||||||
                labelText: 'fieldDescription'.tr(),
 | 
					                labelText: 'fieldDescription'.tr(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onTapOutside: (_) =>
 | 
					              onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            const Gap(12),
 | 
					            const Gap(12),
 | 
				
			||||||
            Row(
 | 
					            Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
 | 
				
			|||||||
import 'package:surface/types/realm.dart';
 | 
					import 'package:surface/types/realm.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';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountPublisherNewScreen extends StatefulWidget {
 | 
					class AccountPublisherNewScreen extends StatefulWidget {
 | 
				
			||||||
  const AccountPublisherNewScreen({super.key});
 | 
					  const AccountPublisherNewScreen({super.key});
 | 
				
			||||||
@@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return  AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAccountPublisherNew').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
@@ -201,7 +206,7 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PublisherNewOrganization extends StatefulWidget {
 | 
					class _PublisherNewOrganization extends StatefulWidget {
 | 
				
			||||||
  const _PublisherNewOrganization({super.key});
 | 
					  const _PublisherNewOrganization();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<_PublisherNewOrganization> createState() =>
 | 
					  State<_PublisherNewOrganization> createState() =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ 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';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PublisherScreen extends StatefulWidget {
 | 
					class PublisherScreen extends StatefulWidget {
 | 
				
			||||||
  const PublisherScreen({super.key});
 | 
					  const PublisherScreen({super.key});
 | 
				
			||||||
@@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final resp = await sn.client.get('/cgi/co/publishers/me');
 | 
					      final resp = await sn.client.get('/cgi/co/publishers/me');
 | 
				
			||||||
      final List<SnPublisher> out = List<SnPublisher>.from(
 | 
					      final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
				
			||||||
          resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAccountPublishers').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: Column(
 | 
					      body: Column(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
@@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
					            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
            leading: const Icon(Symbols.add_circle),
 | 
					            leading: const Icon(Symbols.add_circle),
 | 
				
			||||||
            onTap: () {
 | 
					            onTap: () {
 | 
				
			||||||
              GoRouter.of(context)
 | 
					              GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
 | 
				
			||||||
                  .pushNamed('accountPublisherNew')
 | 
					 | 
				
			||||||
                  .then((value) {
 | 
					 | 
				
			||||||
                if (value == true) {
 | 
					                if (value == true) {
 | 
				
			||||||
                  _publishers.clear();
 | 
					                  _publishers.clear();
 | 
				
			||||||
                  _fetchPublishers();
 | 
					                  _fetchPublishers();
 | 
				
			||||||
@@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
          const Divider(height: 1),
 | 
					          const Divider(height: 1),
 | 
				
			||||||
          LoadingIndicator(isActive: _isBusy),
 | 
					          LoadingIndicator(isActive: _isBusy),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: RefreshIndicator(
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
              onRefresh: () {
 | 
					              context: context,
 | 
				
			||||||
                _publishers.clear();
 | 
					              removeTop: true,
 | 
				
			||||||
                return _fetchPublishers();
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
              },
 | 
					                onRefresh: () {
 | 
				
			||||||
              child: ListView.builder(
 | 
					                  _publishers.clear();
 | 
				
			||||||
                itemCount: _publishers.length,
 | 
					                  return _fetchPublishers();
 | 
				
			||||||
                itemBuilder: (context, idx) {
 | 
					 | 
				
			||||||
                  final publisher = _publishers[idx];
 | 
					 | 
				
			||||||
                  return ListTile(
 | 
					 | 
				
			||||||
                    title: Text(publisher.nick),
 | 
					 | 
				
			||||||
                    subtitle: Text('@${publisher.name}'),
 | 
					 | 
				
			||||||
                    contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
					 | 
				
			||||||
                    leading: AccountImage(content: publisher.avatar),
 | 
					 | 
				
			||||||
                    trailing: PopupMenuButton(
 | 
					 | 
				
			||||||
                      itemBuilder: (BuildContext context) => [
 | 
					 | 
				
			||||||
                        PopupMenuItem(
 | 
					 | 
				
			||||||
                          child: Row(
 | 
					 | 
				
			||||||
                            children: [
 | 
					 | 
				
			||||||
                              const Icon(Symbols.edit),
 | 
					 | 
				
			||||||
                              const Gap(16),
 | 
					 | 
				
			||||||
                              Text('edit').tr(),
 | 
					 | 
				
			||||||
                            ],
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          onTap: () {
 | 
					 | 
				
			||||||
                            GoRouter.of(context).pushNamed(
 | 
					 | 
				
			||||||
                              'accountPublisherEdit',
 | 
					 | 
				
			||||||
                              pathParameters: {
 | 
					 | 
				
			||||||
                                'name': publisher.name,
 | 
					 | 
				
			||||||
                              },
 | 
					 | 
				
			||||||
                            ).then((value) {
 | 
					 | 
				
			||||||
                              if (value == true) {
 | 
					 | 
				
			||||||
                                _publishers.clear();
 | 
					 | 
				
			||||||
                                _fetchPublishers();
 | 
					 | 
				
			||||||
                              }
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                          },
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ],
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                child: ListView.builder(
 | 
				
			||||||
 | 
					                  itemCount: _publishers.length,
 | 
				
			||||||
 | 
					                  itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					                    final publisher = _publishers[idx];
 | 
				
			||||||
 | 
					                    return ListTile(
 | 
				
			||||||
 | 
					                      title: Text(publisher.nick),
 | 
				
			||||||
 | 
					                      subtitle: Text('@${publisher.name}'),
 | 
				
			||||||
 | 
					                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
 | 
					                      leading: AccountImage(content: publisher.avatar),
 | 
				
			||||||
 | 
					                      trailing: PopupMenuButton(
 | 
				
			||||||
 | 
					                        itemBuilder: (BuildContext context) => [
 | 
				
			||||||
 | 
					                          PopupMenuItem(
 | 
				
			||||||
 | 
					                            child: Row(
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                const Icon(Symbols.edit),
 | 
				
			||||||
 | 
					                                const Gap(16),
 | 
				
			||||||
 | 
					                                Text('edit').tr(),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            onTap: () {
 | 
				
			||||||
 | 
					                              GoRouter.of(context).pushNamed(
 | 
				
			||||||
 | 
					                                'accountPublisherEdit',
 | 
				
			||||||
 | 
					                                pathParameters: {
 | 
				
			||||||
 | 
					                                  'name': publisher.name,
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                              ).then((value) {
 | 
				
			||||||
 | 
					                                if (value == true) {
 | 
				
			||||||
 | 
					                                  _publishers.clear();
 | 
				
			||||||
 | 
					                                  _fetchPublishers();
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                              });
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
 | 
				
			|||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
 | 
					import 'package:surface/widgets/attachment/attachment_zoom.dart';
 | 
				
			||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
					import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AlbumScreen extends StatefulWidget {
 | 
					class AlbumScreen extends StatefulWidget {
 | 
				
			||||||
@@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      body: CustomScrollView(
 | 
					      body: CustomScrollView(
 | 
				
			||||||
        controller: _scrollController,
 | 
					        controller: _scrollController,
 | 
				
			||||||
        slivers: [
 | 
					        slivers: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,17 +7,14 @@ import 'package:provider/provider.dart';
 | 
				
			|||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.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/screens/account/factor_settings.dart';
 | 
				
			||||||
import 'package:surface/types/auth.dart';
 | 
					import 'package:surface/types/auth.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../../providers/websocket.dart';
 | 
					import '../../providers/websocket.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
 | 
					 | 
				
			||||||
  0: ('authFactorPassword'.tr(), Symbols.password, false),
 | 
					 | 
				
			||||||
  1: ('authFactorEmail'.tr(), Symbols.email, true),
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LoginScreen extends StatefulWidget {
 | 
					class LoginScreen extends StatefulWidget {
 | 
				
			||||||
  const LoginScreen({super.key});
 | 
					  const LoginScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,67 +32,73 @@ class _LoginScreenState extends State<LoginScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Theme(
 | 
					    return AppScaffold(
 | 
				
			||||||
      data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
 | 
					      appBar: AppBar(
 | 
				
			||||||
      child: SingleChildScrollView(
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
        child: PageTransitionSwitcher(
 | 
					        title: Text('screenAuthLogin').tr(),
 | 
				
			||||||
          transitionBuilder: (
 | 
					      ),
 | 
				
			||||||
            Widget child,
 | 
					      body: Theme(
 | 
				
			||||||
            Animation<double> primaryAnimation,
 | 
					        data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
 | 
				
			||||||
            Animation<double> secondaryAnimation,
 | 
					        child: SingleChildScrollView(
 | 
				
			||||||
          ) {
 | 
					          child: PageTransitionSwitcher(
 | 
				
			||||||
            return SharedAxisTransition(
 | 
					            transitionBuilder: (
 | 
				
			||||||
              animation: primaryAnimation,
 | 
					              Widget child,
 | 
				
			||||||
              secondaryAnimation: secondaryAnimation,
 | 
					              Animation<double> primaryAnimation,
 | 
				
			||||||
              transitionType: SharedAxisTransitionType.horizontal,
 | 
					              Animation<double> secondaryAnimation,
 | 
				
			||||||
              child: Container(
 | 
					            ) {
 | 
				
			||||||
                constraints: BoxConstraints(maxWidth: 380),
 | 
					              return SharedAxisTransition(
 | 
				
			||||||
                child: child,
 | 
					                animation: primaryAnimation,
 | 
				
			||||||
              ),
 | 
					                secondaryAnimation: secondaryAnimation,
 | 
				
			||||||
            );
 | 
					                transitionType: SharedAxisTransitionType.horizontal,
 | 
				
			||||||
          },
 | 
					                child: Container(
 | 
				
			||||||
          child: switch (_period % 3) {
 | 
					                  constraints: BoxConstraints(maxWidth: 380),
 | 
				
			||||||
            1 => _LoginPickerScreen(
 | 
					                  child: child,
 | 
				
			||||||
                key: const ValueKey(1),
 | 
					                ),
 | 
				
			||||||
                ticket: _currentTicket,
 | 
					              );
 | 
				
			||||||
                factors: _factors,
 | 
					            },
 | 
				
			||||||
                onTicket: (p0) => setState(() {
 | 
					            child: switch (_period % 3) {
 | 
				
			||||||
                  _currentTicket = p0;
 | 
					              1 => _LoginPickerScreen(
 | 
				
			||||||
                }),
 | 
					                  key: const ValueKey(1),
 | 
				
			||||||
                onPickFactor: (p0) => setState(() {
 | 
					                  ticket: _currentTicket,
 | 
				
			||||||
                  _factorPicked = p0;
 | 
					                  factors: _factors,
 | 
				
			||||||
                }),
 | 
					                  onTicket: (p0) => setState(() {
 | 
				
			||||||
                onNext: () => setState(() {
 | 
					                    _currentTicket = p0;
 | 
				
			||||||
                  _period++;
 | 
					                  }),
 | 
				
			||||||
                }),
 | 
					                  onPickFactor: (p0) => setState(() {
 | 
				
			||||||
              ),
 | 
					                    _factorPicked = p0;
 | 
				
			||||||
            2 => _LoginCheckScreen(
 | 
					                  }),
 | 
				
			||||||
                key: const ValueKey(2),
 | 
					                  onNext: () => setState(() {
 | 
				
			||||||
                ticket: _currentTicket,
 | 
					                    _period++;
 | 
				
			||||||
                factor: _factorPicked,
 | 
					                  }),
 | 
				
			||||||
                onTicket: (p0) => setState(() {
 | 
					                ),
 | 
				
			||||||
                  _currentTicket = p0;
 | 
					              2 => _LoginCheckScreen(
 | 
				
			||||||
                }),
 | 
					                  key: const ValueKey(2),
 | 
				
			||||||
                onNext: () => setState(() {
 | 
					                  ticket: _currentTicket,
 | 
				
			||||||
                  _period = 1;
 | 
					                  factor: _factorPicked,
 | 
				
			||||||
                }),
 | 
					                  onTicket: (p0) => setState(() {
 | 
				
			||||||
              ),
 | 
					                    _currentTicket = p0;
 | 
				
			||||||
            _ => _LoginLookupScreen(
 | 
					                  }),
 | 
				
			||||||
                key: const ValueKey(0),
 | 
					                  onNext: () => setState(() {
 | 
				
			||||||
                ticket: _currentTicket,
 | 
					                    _period = 1;
 | 
				
			||||||
                onTicket: (p0) => setState(() {
 | 
					                  }),
 | 
				
			||||||
                  _currentTicket = p0;
 | 
					                ),
 | 
				
			||||||
                }),
 | 
					              _ => _LoginLookupScreen(
 | 
				
			||||||
                onFactor: (p0) => setState(() {
 | 
					                  key: const ValueKey(0),
 | 
				
			||||||
                  _factors = p0;
 | 
					                  ticket: _currentTicket,
 | 
				
			||||||
                }),
 | 
					                  onTicket: (p0) => setState(() {
 | 
				
			||||||
                onNext: () => setState(() {
 | 
					                    _currentTicket = p0;
 | 
				
			||||||
                  _period++;
 | 
					                  }),
 | 
				
			||||||
                }),
 | 
					                  onFactor: (p0) => setState(() {
 | 
				
			||||||
              ),
 | 
					                    _factors = p0;
 | 
				
			||||||
          },
 | 
					                  }),
 | 
				
			||||||
        ).padding(all: 24),
 | 
					                  onNext: () => setState(() {
 | 
				
			||||||
      ).center(),
 | 
					                    _period++;
 | 
				
			||||||
 | 
					                  }),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ).padding(all: 24),
 | 
				
			||||||
 | 
					        ).center(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -105,6 +108,7 @@ class _LoginCheckScreen extends StatefulWidget {
 | 
				
			|||||||
  final SnAuthFactor? factor;
 | 
					  final SnAuthFactor? factor;
 | 
				
			||||||
  final Function(SnAuthTicket?) onTicket;
 | 
					  final Function(SnAuthTicket?) onTicket;
 | 
				
			||||||
  final Function onNext;
 | 
					  final Function onNext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _LoginCheckScreen({
 | 
					  const _LoginCheckScreen({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.ticket,
 | 
					    required this.ticket,
 | 
				
			||||||
@@ -204,7 +208,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
 | 
				
			|||||||
          controller: _passwordController,
 | 
					          controller: _passwordController,
 | 
				
			||||||
          obscureText: true,
 | 
					          obscureText: true,
 | 
				
			||||||
          autofillHints: [
 | 
					          autofillHints: [
 | 
				
			||||||
            (_factorLabelMap[widget.factor!.type]?.$3 ?? true)
 | 
					            widget.factor!.type == 0
 | 
				
			||||||
                ? AutofillHints.password
 | 
					                ? AutofillHints.password
 | 
				
			||||||
                : AutofillHints.oneTimeCode
 | 
					                : AutofillHints.oneTimeCode
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
@@ -243,6 +247,7 @@ class _LoginPickerScreen extends StatefulWidget {
 | 
				
			|||||||
  final Function(SnAuthTicket?) onTicket;
 | 
					  final Function(SnAuthTicket?) onTicket;
 | 
				
			||||||
  final Function(SnAuthFactor) onPickFactor;
 | 
					  final Function(SnAuthFactor) onPickFactor;
 | 
				
			||||||
  final Function onNext;
 | 
					  final Function onNext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _LoginPickerScreen({
 | 
					  const _LoginPickerScreen({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.ticket,
 | 
					    required this.ticket,
 | 
				
			||||||
@@ -322,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
 | 
				
			|||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        secondary: Icon(
 | 
					                        secondary: Icon(
 | 
				
			||||||
                          _factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
 | 
					                          kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        title: Text(
 | 
					                        title: Text(
 | 
				
			||||||
                          _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
 | 
					                          kFactorTypes[x.type]?.$1 ?? 'unknown',
 | 
				
			||||||
                        ),
 | 
					                        ).tr(),
 | 
				
			||||||
                        enabled: !widget.ticket!.factorTrail.contains(x.id),
 | 
					                        enabled: !widget.ticket!.factorTrail.contains(x.id),
 | 
				
			||||||
                        value: _factorPicked == x.id,
 | 
					                        value: _factorPicked == x.id,
 | 
				
			||||||
                        onChanged: (value) {
 | 
					                        onChanged: (value) {
 | 
				
			||||||
@@ -373,6 +378,7 @@ class _LoginLookupScreen extends StatefulWidget {
 | 
				
			|||||||
  final Function(SnAuthTicket?) onTicket;
 | 
					  final Function(SnAuthTicket?) onTicket;
 | 
				
			||||||
  final Function(List<SnAuthFactor>?) onFactor;
 | 
					  final Function(List<SnAuthFactor>?) onFactor;
 | 
				
			||||||
  final Function onNext;
 | 
					  final Function onNext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _LoginLookupScreen({
 | 
					  const _LoginLookupScreen({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.ticket,
 | 
					    required this.ticket,
 | 
				
			||||||
@@ -406,9 +412,11 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
 | 
				
			|||||||
      await sn.client.post('/cgi/id/users/me/password-reset', data: {
 | 
					      await sn.client.post('/cgi/id/users/me/password-reset', data: {
 | 
				
			||||||
        'user_id': lookupResp.data['id'],
 | 
					        'user_id': lookupResp.data['id'],
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
 | 
					      if (mounted) {
 | 
				
			||||||
 | 
					        context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -443,7 +451,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      widget.onNext();
 | 
					      widget.onNext();
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
 | 
				
			|||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RegisterScreen extends StatefulWidget {
 | 
					class RegisterScreen extends StatefulWidget {
 | 
				
			||||||
@@ -43,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
				
			|||||||
        'nick': nickname,
 | 
					        'nick': nickname,
 | 
				
			||||||
        'email': email,
 | 
					        'email': email,
 | 
				
			||||||
        'password': password,
 | 
					        'password': password,
 | 
				
			||||||
 | 
					        'language': EasyLocalization.of(context)!.currentLocale.toString(),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!context.mounted) return;
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
@@ -54,175 +56,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return StyledWidget(Container(
 | 
					    return AppScaffold(
 | 
				
			||||||
      constraints: const BoxConstraints(maxWidth: 380),
 | 
					      appBar: AppBar(
 | 
				
			||||||
      child: SingleChildScrollView(
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
        child: Column(
 | 
					        title: Text('screenAuthRegister').tr(),
 | 
				
			||||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
					      ),
 | 
				
			||||||
          children: [
 | 
					      body: StyledWidget(Container(
 | 
				
			||||||
            Align(
 | 
					        constraints: const BoxConstraints(maxWidth: 380),
 | 
				
			||||||
              alignment: Alignment.centerLeft,
 | 
					        child: SingleChildScrollView(
 | 
				
			||||||
              child: CircleAvatar(
 | 
					          child: Column(
 | 
				
			||||||
                radius: 26,
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                child: const Icon(
 | 
					            children: [
 | 
				
			||||||
                  Symbols.person_add,
 | 
					              Align(
 | 
				
			||||||
                  size: 28,
 | 
					                alignment: Alignment.centerLeft,
 | 
				
			||||||
                ),
 | 
					                child: CircleAvatar(
 | 
				
			||||||
              ).padding(bottom: 8),
 | 
					                  radius: 26,
 | 
				
			||||||
            ),
 | 
					                  child: const Icon(
 | 
				
			||||||
            Text(
 | 
					                    Symbols.person_add,
 | 
				
			||||||
              'screenAuthRegister',
 | 
					                    size: 28,
 | 
				
			||||||
              style: const TextStyle(
 | 
					                  ),
 | 
				
			||||||
                fontSize: 28,
 | 
					                ).padding(bottom: 8),
 | 
				
			||||||
                fontWeight: FontWeight.w900,
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ).tr().padding(left: 4, bottom: 16),
 | 
					              Text(
 | 
				
			||||||
            Form(
 | 
					                'screenAuthRegister',
 | 
				
			||||||
              key: _formKey,
 | 
					                style: const TextStyle(
 | 
				
			||||||
              autovalidateMode: AutovalidateMode.onUserInteraction,
 | 
					                  fontSize: 28,
 | 
				
			||||||
              child: Column(
 | 
					                  fontWeight: FontWeight.w900,
 | 
				
			||||||
                children: [
 | 
					                ),
 | 
				
			||||||
                  TextFormField(
 | 
					              ).tr().padding(left: 4, bottom: 16),
 | 
				
			||||||
                    validator: (value) {
 | 
					              Form(
 | 
				
			||||||
                      if (value == null || value.length < 4 || value.length > 32) {
 | 
					                key: _formKey,
 | 
				
			||||||
                        return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
					                autovalidateMode: AutovalidateMode.onUserInteraction,
 | 
				
			||||||
                      }
 | 
					                child: Column(
 | 
				
			||||||
                      if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
 | 
					                  children: [
 | 
				
			||||||
                        return 'fieldUsernameAlphanumOnly'.tr();
 | 
					                    TextFormField(
 | 
				
			||||||
                      }
 | 
					                      validator: (value) {
 | 
				
			||||||
                      return null;
 | 
					                        if (value == null || value.length < 4 || value.length > 32) {
 | 
				
			||||||
                    },
 | 
					                          return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
				
			||||||
                    autocorrect: false,
 | 
					                        }
 | 
				
			||||||
                    enableSuggestions: false,
 | 
					                        if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
 | 
				
			||||||
                    controller: _usernameController,
 | 
					                          return 'fieldUsernameAlphanumOnly'.tr();
 | 
				
			||||||
                    autofillHints: const [AutofillHints.username],
 | 
					                        }
 | 
				
			||||||
                    decoration: InputDecoration(
 | 
					                        return null;
 | 
				
			||||||
                      isDense: true,
 | 
					                      },
 | 
				
			||||||
                      border: const UnderlineInputBorder(),
 | 
					                      autocorrect: false,
 | 
				
			||||||
                      labelText: 'fieldUsername'.tr(),
 | 
					                      enableSuggestions: false,
 | 
				
			||||||
                    ),
 | 
					                      controller: _usernameController,
 | 
				
			||||||
                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                      autofillHints: const [AutofillHints.username],
 | 
				
			||||||
                  ),
 | 
					                      decoration: InputDecoration(
 | 
				
			||||||
                  const Gap(12),
 | 
					                        isDense: true,
 | 
				
			||||||
                  TextFormField(
 | 
					                        border: const UnderlineInputBorder(),
 | 
				
			||||||
                    validator: (value) {
 | 
					                        labelText: 'fieldUsername'.tr(),
 | 
				
			||||||
                      if (value == null || value.length < 4 || value.length > 32) {
 | 
					 | 
				
			||||||
                        return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      return null;
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    autocorrect: false,
 | 
					 | 
				
			||||||
                    enableSuggestions: false,
 | 
					 | 
				
			||||||
                    controller: _nicknameController,
 | 
					 | 
				
			||||||
                    autofillHints: const [AutofillHints.nickname],
 | 
					 | 
				
			||||||
                    decoration: InputDecoration(
 | 
					 | 
				
			||||||
                      isDense: true,
 | 
					 | 
				
			||||||
                      border: const UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                      labelText: 'fieldNickname'.tr(),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  const Gap(12),
 | 
					 | 
				
			||||||
                  TextFormField(
 | 
					 | 
				
			||||||
                    validator: (value) {
 | 
					 | 
				
			||||||
                      if (value == null || value.isEmpty) {
 | 
					 | 
				
			||||||
                        return 'fieldCannotBeEmpty'.tr();
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      if (!EmailValidator.validate(value)) {
 | 
					 | 
				
			||||||
                        return 'fieldEmailAddressMustBeValid'.tr();
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      return null;
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    autocorrect: false,
 | 
					 | 
				
			||||||
                    enableSuggestions: false,
 | 
					 | 
				
			||||||
                    controller: _emailController,
 | 
					 | 
				
			||||||
                    autofillHints: const [AutofillHints.email],
 | 
					 | 
				
			||||||
                    decoration: InputDecoration(
 | 
					 | 
				
			||||||
                      isDense: true,
 | 
					 | 
				
			||||||
                      border: const UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                      labelText: 'fieldEmail'.tr(),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  const Gap(12),
 | 
					 | 
				
			||||||
                  TextFormField(
 | 
					 | 
				
			||||||
                    validator: (value) {
 | 
					 | 
				
			||||||
                      if (value == null || value.isEmpty) {
 | 
					 | 
				
			||||||
                        return 'fieldCannotBeEmpty'.tr();
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      return null;
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    obscureText: true,
 | 
					 | 
				
			||||||
                    autocorrect: false,
 | 
					 | 
				
			||||||
                    enableSuggestions: false,
 | 
					 | 
				
			||||||
                    autofillHints: const [AutofillHints.password],
 | 
					 | 
				
			||||||
                    controller: _passwordController,
 | 
					 | 
				
			||||||
                    decoration: InputDecoration(
 | 
					 | 
				
			||||||
                      isDense: true,
 | 
					 | 
				
			||||||
                      border: const UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                      labelText: 'fieldPassword'.tr(),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
              ).padding(horizontal: 7),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            const Gap(16),
 | 
					 | 
				
			||||||
            Align(
 | 
					 | 
				
			||||||
              alignment: Alignment.centerRight,
 | 
					 | 
				
			||||||
              child: StyledWidget(
 | 
					 | 
				
			||||||
                Container(
 | 
					 | 
				
			||||||
                  constraints: const BoxConstraints(maxWidth: 290),
 | 
					 | 
				
			||||||
                  child: Column(
 | 
					 | 
				
			||||||
                    crossAxisAlignment: CrossAxisAlignment.end,
 | 
					 | 
				
			||||||
                    children: [
 | 
					 | 
				
			||||||
                      Text(
 | 
					 | 
				
			||||||
                        'termAcceptNextWithAgree'.tr(),
 | 
					 | 
				
			||||||
                        textAlign: TextAlign.end,
 | 
					 | 
				
			||||||
                        style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
					 | 
				
			||||||
                          color: Theme.of(context)
 | 
					 | 
				
			||||||
                              .colorScheme
 | 
					 | 
				
			||||||
                              .onSurface
 | 
					 | 
				
			||||||
                              .withAlpha((255 * 0.75).round()),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      Material(
 | 
					                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                        color: Colors.transparent,
 | 
					                    ),
 | 
				
			||||||
                        child: InkWell(
 | 
					                    const Gap(12),
 | 
				
			||||||
                          child: Row(
 | 
					                    TextFormField(
 | 
				
			||||||
                            mainAxisSize: MainAxisSize.min,
 | 
					                      validator: (value) {
 | 
				
			||||||
                            children: [
 | 
					                        if (value == null || value.length < 4 || value.length > 32) {
 | 
				
			||||||
                              Text('termAcceptLink'.tr()),
 | 
					                          return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
				
			||||||
                              const Gap(4),
 | 
					                        }
 | 
				
			||||||
                              const Icon(Symbols.launch, size: 14),
 | 
					                        return null;
 | 
				
			||||||
                            ],
 | 
					                      },
 | 
				
			||||||
 | 
					                      autocorrect: false,
 | 
				
			||||||
 | 
					                      enableSuggestions: false,
 | 
				
			||||||
 | 
					                      controller: _nicknameController,
 | 
				
			||||||
 | 
					                      autofillHints: const [AutofillHints.nickname],
 | 
				
			||||||
 | 
					                      decoration: InputDecoration(
 | 
				
			||||||
 | 
					                        isDense: true,
 | 
				
			||||||
 | 
					                        border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                        labelText: 'fieldNickname'.tr(),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const Gap(12),
 | 
				
			||||||
 | 
					                    TextFormField(
 | 
				
			||||||
 | 
					                      validator: (value) {
 | 
				
			||||||
 | 
					                        if (value == null || value.isEmpty) {
 | 
				
			||||||
 | 
					                          return 'fieldCannotBeEmpty'.tr();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (!EmailValidator.validate(value)) {
 | 
				
			||||||
 | 
					                          return 'fieldEmailAddressMustBeValid'.tr();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      autocorrect: false,
 | 
				
			||||||
 | 
					                      enableSuggestions: false,
 | 
				
			||||||
 | 
					                      controller: _emailController,
 | 
				
			||||||
 | 
					                      autofillHints: const [AutofillHints.email],
 | 
				
			||||||
 | 
					                      decoration: InputDecoration(
 | 
				
			||||||
 | 
					                        isDense: true,
 | 
				
			||||||
 | 
					                        border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                        labelText: 'fieldEmail'.tr(),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const Gap(12),
 | 
				
			||||||
 | 
					                    TextFormField(
 | 
				
			||||||
 | 
					                      validator: (value) {
 | 
				
			||||||
 | 
					                        if (value == null || value.isEmpty) {
 | 
				
			||||||
 | 
					                          return 'fieldCannotBeEmpty'.tr();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      obscureText: true,
 | 
				
			||||||
 | 
					                      autocorrect: false,
 | 
				
			||||||
 | 
					                      enableSuggestions: false,
 | 
				
			||||||
 | 
					                      autofillHints: const [AutofillHints.password],
 | 
				
			||||||
 | 
					                      controller: _passwordController,
 | 
				
			||||||
 | 
					                      decoration: InputDecoration(
 | 
				
			||||||
 | 
					                        isDense: true,
 | 
				
			||||||
 | 
					                        border: const UnderlineInputBorder(),
 | 
				
			||||||
 | 
					                        labelText: 'fieldPassword'.tr(),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ).padding(horizontal: 7),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              const Gap(16),
 | 
				
			||||||
 | 
					              Align(
 | 
				
			||||||
 | 
					                alignment: Alignment.centerRight,
 | 
				
			||||||
 | 
					                child: StyledWidget(
 | 
				
			||||||
 | 
					                  Container(
 | 
				
			||||||
 | 
					                    constraints: const BoxConstraints(maxWidth: 290),
 | 
				
			||||||
 | 
					                    child: Column(
 | 
				
			||||||
 | 
					                      crossAxisAlignment: CrossAxisAlignment.end,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Text(
 | 
				
			||||||
 | 
					                          'termAcceptNextWithAgree'.tr(),
 | 
				
			||||||
 | 
					                          textAlign: TextAlign.end,
 | 
				
			||||||
 | 
					                          style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
				
			||||||
 | 
					                                color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        Material(
 | 
				
			||||||
 | 
					                          color: Colors.transparent,
 | 
				
			||||||
 | 
					                          child: InkWell(
 | 
				
			||||||
 | 
					                            child: Row(
 | 
				
			||||||
 | 
					                              mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                Text('termAcceptLink'.tr()),
 | 
				
			||||||
 | 
					                                const Gap(4),
 | 
				
			||||||
 | 
					                                const Icon(Symbols.launch, size: 14),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            onTap: () {
 | 
				
			||||||
 | 
					                              launchUrlString('https://solsynth.dev/terms');
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                          onTap: () {
 | 
					 | 
				
			||||||
                            launchUrlString('https://solsynth.dev/terms');
 | 
					 | 
				
			||||||
                          },
 | 
					 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              Align(
 | 
				
			||||||
 | 
					                alignment: Alignment.centerRight,
 | 
				
			||||||
 | 
					                child: TextButton(
 | 
				
			||||||
 | 
					                  onPressed: () => _performAction(context),
 | 
				
			||||||
 | 
					                  child: Row(
 | 
				
			||||||
 | 
					                    mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Text('next').tr(),
 | 
				
			||||||
 | 
					                      const Icon(Symbols.chevron_right),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ).padding(horizontal: 16),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            Align(
 | 
					 | 
				
			||||||
              alignment: Alignment.centerRight,
 | 
					 | 
				
			||||||
              child: TextButton(
 | 
					 | 
				
			||||||
                onPressed: () => _performAction(context),
 | 
					 | 
				
			||||||
                child: Row(
 | 
					 | 
				
			||||||
                  mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                  children: [
 | 
					 | 
				
			||||||
                    Text('next').tr(),
 | 
					 | 
				
			||||||
                    const Icon(Symbols.chevron_right),
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ],
 | 
				
			||||||
          ],
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      )).padding(all: 24).center(),
 | 
				
			||||||
    )).padding(all: 24).center();
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.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/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:surface/widgets/unauthorized_hint.dart';
 | 
					import 'package:surface/widgets/unauthorized_hint.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
    final ua = context.read<UserProvider>();
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!ua.isAuthorized) {
 | 
					    if (!ua.isAuthorized) {
 | 
				
			||||||
      return Scaffold(
 | 
					      return AppScaffold(
 | 
				
			||||||
        appBar: AppBar(
 | 
					        appBar: AppBar(
 | 
				
			||||||
          leading: AutoAppBarLeading(),
 | 
					          leading: AutoAppBarLeading(),
 | 
				
			||||||
          title: Text('screenChat').tr(),
 | 
					          title: Text('screenChat').tr(),
 | 
				
			||||||
@@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text('screenChat').tr(),
 | 
					        title: Text('screenChat').tr(),
 | 
				
			||||||
@@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          LoadingIndicator(isActive: _isBusy),
 | 
					          LoadingIndicator(isActive: _isBusy),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: RefreshIndicator(
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
              onRefresh: () => Future.sync(() => _refreshChannels()),
 | 
					              context: context,
 | 
				
			||||||
              child: ListView.builder(
 | 
					              removeTop: true,
 | 
				
			||||||
                itemCount: _channels?.length ?? 0,
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
                itemBuilder: (context, idx) {
 | 
					                onRefresh: () => Future.sync(() => _refreshChannels()),
 | 
				
			||||||
                  final channel = _channels![idx];
 | 
					                child: ListView.builder(
 | 
				
			||||||
                  final lastMessage = _lastMessages?[channel.id];
 | 
					                  itemCount: _channels?.length ?? 0,
 | 
				
			||||||
 | 
					                  itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					                    final channel = _channels![idx];
 | 
				
			||||||
 | 
					                    final lastMessage = _lastMessages?[channel.id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  if (channel.type == 1) {
 | 
					                    if (channel.type == 1) {
 | 
				
			||||||
                    final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
 | 
					                      final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
 | 
				
			||||||
                          (ele) => ele?.accountId != ua.user?.id,
 | 
					                            (ele) => ele?.accountId != ua.user?.id,
 | 
				
			||||||
                          orElse: () => null,
 | 
					                            orElse: () => null,
 | 
				
			||||||
                        );
 | 
					                          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      return ListTile(
 | 
				
			||||||
 | 
					                        title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
 | 
				
			||||||
 | 
					                        subtitle: lastMessage != null
 | 
				
			||||||
 | 
					                            ? Text(
 | 
				
			||||||
 | 
					                                '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
 | 
				
			||||||
 | 
					                                maxLines: 1,
 | 
				
			||||||
 | 
					                                overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                              )
 | 
				
			||||||
 | 
					                            : Text(
 | 
				
			||||||
 | 
					                                'channelDirectMessageDescription'.tr(args: [
 | 
				
			||||||
 | 
					                                  '@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
 | 
				
			||||||
 | 
					                                ]),
 | 
				
			||||||
 | 
					                                maxLines: 1,
 | 
				
			||||||
 | 
					                                overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                        contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
 | 
					                        leading: AccountImage(
 | 
				
			||||||
 | 
					                          content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        onTap: () {
 | 
				
			||||||
 | 
					                          GoRouter.of(context).pushNamed(
 | 
				
			||||||
 | 
					                            'chatRoom',
 | 
				
			||||||
 | 
					                            pathParameters: {
 | 
				
			||||||
 | 
					                              'scope': channel.realm?.alias ?? 'global',
 | 
				
			||||||
 | 
					                              'alias': channel.alias,
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ).then((value) {
 | 
				
			||||||
 | 
					                            if (mounted) _refreshChannels();
 | 
				
			||||||
 | 
					                          });
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return ListTile(
 | 
					                    return ListTile(
 | 
				
			||||||
                      title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
 | 
					                      title: Text(channel.name),
 | 
				
			||||||
                      subtitle: lastMessage != null
 | 
					                      subtitle: lastMessage != null
 | 
				
			||||||
                          ? Text(
 | 
					                          ? Text(
 | 
				
			||||||
                              '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
 | 
					                              '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
 | 
				
			||||||
@@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
                              overflow: TextOverflow.ellipsis,
 | 
					                              overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                          : Text(
 | 
					                          : Text(
 | 
				
			||||||
                              'channelDirectMessageDescription'.tr(args: [
 | 
					                              channel.description,
 | 
				
			||||||
                                '@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
 | 
					 | 
				
			||||||
                              ]),
 | 
					 | 
				
			||||||
                              maxLines: 1,
 | 
					                              maxLines: 1,
 | 
				
			||||||
                              overflow: TextOverflow.ellipsis,
 | 
					                              overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
					                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
                      leading: AccountImage(
 | 
					                      leading: AccountImage(
 | 
				
			||||||
                        content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
 | 
					                        content: null,
 | 
				
			||||||
 | 
					                        fallbackWidget: const Icon(Symbols.chat, size: 20),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      onTap: () {
 | 
					                      onTap: () {
 | 
				
			||||||
                        GoRouter.of(context).pushNamed(
 | 
					                        GoRouter.of(context).pushNamed(
 | 
				
			||||||
@@ -240,39 +276,8 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
                        });
 | 
					                        });
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                  }
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
                  return ListTile(
 | 
					 | 
				
			||||||
                    title: Text(channel.name),
 | 
					 | 
				
			||||||
                    subtitle: lastMessage != null
 | 
					 | 
				
			||||||
                        ? Text(
 | 
					 | 
				
			||||||
                            '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
 | 
					 | 
				
			||||||
                            maxLines: 1,
 | 
					 | 
				
			||||||
                            overflow: TextOverflow.ellipsis,
 | 
					 | 
				
			||||||
                          )
 | 
					 | 
				
			||||||
                        : Text(
 | 
					 | 
				
			||||||
                            channel.description,
 | 
					 | 
				
			||||||
                            maxLines: 1,
 | 
					 | 
				
			||||||
                            overflow: TextOverflow.ellipsis,
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                    contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
					 | 
				
			||||||
                    leading: AccountImage(
 | 
					 | 
				
			||||||
                      content: null,
 | 
					 | 
				
			||||||
                      fallbackWidget: const Icon(Symbols.chat, size: 20),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    onTap: () {
 | 
					 | 
				
			||||||
                      GoRouter.of(context).pushNamed(
 | 
					 | 
				
			||||||
                        'chatRoom',
 | 
					 | 
				
			||||||
                        pathParameters: {
 | 
					 | 
				
			||||||
                          'scope': channel.realm?.alias ?? 'global',
 | 
					 | 
				
			||||||
                          'alias': channel.alias,
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                      ).then((value) {
 | 
					 | 
				
			||||||
                        if (value == true) _refreshChannels();
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/chat_call.dart';
 | 
					import 'package:surface/providers/chat_call.dart';
 | 
				
			||||||
import 'package:surface/widgets/chat/call/call_controls.dart';
 | 
					import 'package:surface/widgets/chat/call/call_controls.dart';
 | 
				
			||||||
import 'package:surface/widgets/chat/call/call_participant.dart';
 | 
					import 'package:surface/widgets/chat/call/call_participant.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CallRoomScreen extends StatefulWidget {
 | 
					class CallRoomScreen extends StatefulWidget {
 | 
				
			||||||
  final String scope;
 | 
					  final String scope;
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const CallRoomScreen({super.key, required this.scope, required this.alias});
 | 
					  const CallRoomScreen({super.key, required this.scope, required this.alias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        Container(
 | 
					        Container(
 | 
				
			||||||
          color:
 | 
					          color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
				
			||||||
              Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
					 | 
				
			||||||
          child: call.focusTrack != null
 | 
					          child: call.focusTrack != null
 | 
				
			||||||
              ? InteractiveParticipantWidget(
 | 
					              ? InteractiveParticipantWidget(
 | 
				
			||||||
                  isFixedAvatar: false,
 | 
					                  isFixedAvatar: false,
 | 
				
			||||||
@@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
                      color: Theme.of(context).cardColor,
 | 
					                      color: Theme.of(context).cardColor,
 | 
				
			||||||
                      participant: track,
 | 
					                      participant: track,
 | 
				
			||||||
                      onTap: () {
 | 
					                      onTap: () {
 | 
				
			||||||
                        if (track.participant.sid !=
 | 
					                        if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
				
			||||||
                            call.focusTrack?.participant.sid) {
 | 
					 | 
				
			||||||
                          call.setFocusTrack(track);
 | 
					                          call.setFocusTrack(track);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
@@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
            child: ClipRRect(
 | 
					            child: ClipRRect(
 | 
				
			||||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
              child: InteractiveParticipantWidget(
 | 
					              child: InteractiveParticipantWidget(
 | 
				
			||||||
                color: Theme.of(context)
 | 
					                color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
 | 
				
			||||||
                    .colorScheme
 | 
					 | 
				
			||||||
                    .surfaceContainerHigh
 | 
					 | 
				
			||||||
                    .withOpacity(0.75),
 | 
					 | 
				
			||||||
                participant: track,
 | 
					                participant: track,
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  if (track.participant.sid !=
 | 
					                  if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
				
			||||||
                      call.focusTrack?.participant.sid) {
 | 
					 | 
				
			||||||
                    call.setFocusTrack(track);
 | 
					                    call.setFocusTrack(track);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
@@ -152,157 +148,138 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
    return ListenableBuilder(
 | 
					    return ListenableBuilder(
 | 
				
			||||||
        listenable: call,
 | 
					        listenable: call,
 | 
				
			||||||
        builder: (context, _) {
 | 
					        builder: (context, _) {
 | 
				
			||||||
          return Scaffold(
 | 
					          return AppScaffold(
 | 
				
			||||||
            appBar: AppBar(
 | 
					            appBar: AppBar(
 | 
				
			||||||
              title: RichText(
 | 
					              title: RichText(
 | 
				
			||||||
                textAlign: TextAlign.center,
 | 
					                textAlign: TextAlign.center,
 | 
				
			||||||
                text: TextSpan(children: [
 | 
					                text: TextSpan(children: [
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: 'call'.tr(),
 | 
					                    text: 'call'.tr(),
 | 
				
			||||||
                    style: Theme.of(context)
 | 
					                    style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
				
			||||||
                        .textTheme
 | 
					                          color: Theme.of(context).appBarTheme.foregroundColor,
 | 
				
			||||||
                        .titleLarge!
 | 
					                        ),
 | 
				
			||||||
                        .copyWith(color: Colors.white),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const TextSpan(text: '\n'),
 | 
					                  const TextSpan(text: '\n'),
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: call.lastDuration.toString(),
 | 
					                    text: call.lastDuration.toString(),
 | 
				
			||||||
                    style: Theme.of(context)
 | 
					                    style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
				
			||||||
                        .textTheme
 | 
					                          color: Theme.of(context).appBarTheme.foregroundColor,
 | 
				
			||||||
                        .bodySmall!
 | 
					                        ),
 | 
				
			||||||
                        .copyWith(color: Colors.white),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ]),
 | 
					                ]),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            body: SafeArea(
 | 
					            body: GestureDetector(
 | 
				
			||||||
              child: GestureDetector(
 | 
					              behavior: HitTestBehavior.translucent,
 | 
				
			||||||
                behavior: HitTestBehavior.translucent,
 | 
					              child: Column(
 | 
				
			||||||
                child: Column(
 | 
					                children: [
 | 
				
			||||||
                  children: [
 | 
					                  SizedBox(
 | 
				
			||||||
 | 
					                    width: MediaQuery.of(context).size.width,
 | 
				
			||||||
 | 
					                    height: 64,
 | 
				
			||||||
 | 
					                    child: Row(
 | 
				
			||||||
 | 
					                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                      crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Builder(builder: (context) {
 | 
				
			||||||
 | 
					                          final call = context.read<ChatCallProvider>();
 | 
				
			||||||
 | 
					                          final connectionQuality =
 | 
				
			||||||
 | 
					                              call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
 | 
				
			||||||
 | 
					                          return Expanded(
 | 
				
			||||||
 | 
					                            child: Column(
 | 
				
			||||||
 | 
					                              mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                Row(
 | 
				
			||||||
 | 
					                                  children: [
 | 
				
			||||||
 | 
					                                    Text(
 | 
				
			||||||
 | 
					                                      call.channel?.name ?? 'unknown'.tr(),
 | 
				
			||||||
 | 
					                                      style: const TextStyle(
 | 
				
			||||||
 | 
					                                        fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    const Gap(6),
 | 
				
			||||||
 | 
					                                    Text(call.lastDuration.toString())
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                                Row(
 | 
				
			||||||
 | 
					                                  children: [
 | 
				
			||||||
 | 
					                                    Text(
 | 
				
			||||||
 | 
					                                      {
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
 | 
				
			||||||
 | 
					                                      }[call.room.connectionState]!,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    const Gap(6),
 | 
				
			||||||
 | 
					                                    if (connectionQuality != livekit.ConnectionQuality.unknown)
 | 
				
			||||||
 | 
					                                      Icon(
 | 
				
			||||||
 | 
					                                        {
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
 | 
				
			||||||
 | 
					                                        }[connectionQuality],
 | 
				
			||||||
 | 
					                                        color: {
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.excellent: Colors.green,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.good: Colors.orange,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.poor: Colors.red,
 | 
				
			||||||
 | 
					                                        }[connectionQuality],
 | 
				
			||||||
 | 
					                                        size: 16,
 | 
				
			||||||
 | 
					                                      )
 | 
				
			||||||
 | 
					                                    else
 | 
				
			||||||
 | 
					                                      const SizedBox(
 | 
				
			||||||
 | 
					                                        width: 12,
 | 
				
			||||||
 | 
					                                        height: 12,
 | 
				
			||||||
 | 
					                                        child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					                                          color: Colors.white,
 | 
				
			||||||
 | 
					                                          strokeWidth: 2,
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ).padding(all: 3),
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        }),
 | 
				
			||||||
 | 
					                        Row(
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            IconButton(
 | 
				
			||||||
 | 
					                              icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
 | 
				
			||||||
 | 
					                              onPressed: () {
 | 
				
			||||||
 | 
					                                _switchLayout();
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ).padding(left: 20, right: 16),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  Expanded(
 | 
				
			||||||
 | 
					                    child: Material(
 | 
				
			||||||
 | 
					                      color: Theme.of(context).colorScheme.surfaceContainerLow,
 | 
				
			||||||
 | 
					                      child: Builder(
 | 
				
			||||||
 | 
					                        builder: (context) {
 | 
				
			||||||
 | 
					                          switch (_layoutMode) {
 | 
				
			||||||
 | 
					                            case 1:
 | 
				
			||||||
 | 
					                              return _buildGridLayout();
 | 
				
			||||||
 | 
					                            default:
 | 
				
			||||||
 | 
					                              return _buildListLayout();
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  if (call.room.localParticipant != null)
 | 
				
			||||||
                    SizedBox(
 | 
					                    SizedBox(
 | 
				
			||||||
                      width: MediaQuery.of(context).size.width,
 | 
					                      width: MediaQuery.of(context).size.width,
 | 
				
			||||||
                      height: 64,
 | 
					                      child: ControlsWidget(
 | 
				
			||||||
                      child: Row(
 | 
					                        call.room,
 | 
				
			||||||
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					                        call.room.localParticipant!,
 | 
				
			||||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          Builder(builder: (context) {
 | 
					 | 
				
			||||||
                            final call = context.read<ChatCallProvider>();
 | 
					 | 
				
			||||||
                            final connectionQuality =
 | 
					 | 
				
			||||||
                                call.room.localParticipant?.connectionQuality ??
 | 
					 | 
				
			||||||
                                    livekit.ConnectionQuality.unknown;
 | 
					 | 
				
			||||||
                            return Expanded(
 | 
					 | 
				
			||||||
                              child: Column(
 | 
					 | 
				
			||||||
                                mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                                crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                                children: [
 | 
					 | 
				
			||||||
                                  Row(
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      Text(
 | 
					 | 
				
			||||||
                                        call.channel?.name ?? 'unknown'.tr(),
 | 
					 | 
				
			||||||
                                        style: const TextStyle(
 | 
					 | 
				
			||||||
                                          fontWeight: FontWeight.bold,
 | 
					 | 
				
			||||||
                                        ),
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      const Gap(6),
 | 
					 | 
				
			||||||
                                      Text(call.lastDuration.toString())
 | 
					 | 
				
			||||||
                                    ],
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                  Row(
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      Text(
 | 
					 | 
				
			||||||
                                        {
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.disconnected:
 | 
					 | 
				
			||||||
                                              'callStatusDisconnected'.tr(),
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.connected:
 | 
					 | 
				
			||||||
                                              'callStatusConnected'.tr(),
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.connecting:
 | 
					 | 
				
			||||||
                                              'callStatusConnecting'.tr(),
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.reconnecting:
 | 
					 | 
				
			||||||
                                              'callStatusReconnecting'.tr(),
 | 
					 | 
				
			||||||
                                        }[call.room.connectionState]!,
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      const Gap(6),
 | 
					 | 
				
			||||||
                                      if (connectionQuality !=
 | 
					 | 
				
			||||||
                                          livekit.ConnectionQuality.unknown)
 | 
					 | 
				
			||||||
                                        Icon(
 | 
					 | 
				
			||||||
                                          {
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.excellent:
 | 
					 | 
				
			||||||
                                                Icons.signal_cellular_alt,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.good:
 | 
					 | 
				
			||||||
                                                Icons.signal_cellular_alt_2_bar,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.poor:
 | 
					 | 
				
			||||||
                                                Icons.signal_cellular_alt_1_bar,
 | 
					 | 
				
			||||||
                                          }[connectionQuality],
 | 
					 | 
				
			||||||
                                          color: {
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.excellent:
 | 
					 | 
				
			||||||
                                                Colors.green,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.good:
 | 
					 | 
				
			||||||
                                                Colors.orange,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.poor:
 | 
					 | 
				
			||||||
                                                Colors.red,
 | 
					 | 
				
			||||||
                                          }[connectionQuality],
 | 
					 | 
				
			||||||
                                          size: 16,
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                      else
 | 
					 | 
				
			||||||
                                        const SizedBox(
 | 
					 | 
				
			||||||
                                          width: 12,
 | 
					 | 
				
			||||||
                                          height: 12,
 | 
					 | 
				
			||||||
                                          child: CircularProgressIndicator(
 | 
					 | 
				
			||||||
                                            color: Colors.white,
 | 
					 | 
				
			||||||
                                            strokeWidth: 2,
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                        ).padding(all: 3),
 | 
					 | 
				
			||||||
                                    ],
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                ],
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                          }),
 | 
					 | 
				
			||||||
                          Row(
 | 
					 | 
				
			||||||
                            children: [
 | 
					 | 
				
			||||||
                              IconButton(
 | 
					 | 
				
			||||||
                                icon: _layoutMode == 0
 | 
					 | 
				
			||||||
                                    ? const Icon(Icons.view_list)
 | 
					 | 
				
			||||||
                                    : const Icon(Icons.grid_view),
 | 
					 | 
				
			||||||
                                onPressed: () {
 | 
					 | 
				
			||||||
                                  _switchLayout();
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            ],
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ).padding(left: 20, right: 16),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    Expanded(
 | 
					 | 
				
			||||||
                      child: Material(
 | 
					 | 
				
			||||||
                        color:
 | 
					 | 
				
			||||||
                            Theme.of(context).colorScheme.surfaceContainerLow,
 | 
					 | 
				
			||||||
                        child: Builder(
 | 
					 | 
				
			||||||
                          builder: (context) {
 | 
					 | 
				
			||||||
                            switch (_layoutMode) {
 | 
					 | 
				
			||||||
                              case 1:
 | 
					 | 
				
			||||||
                                return _buildGridLayout();
 | 
					 | 
				
			||||||
                              default:
 | 
					 | 
				
			||||||
                                return _buildListLayout();
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                          },
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    if (call.room.localParticipant != null)
 | 
					                ],
 | 
				
			||||||
                      SizedBox(
 | 
					 | 
				
			||||||
                        width: MediaQuery.of(context).size.width,
 | 
					 | 
				
			||||||
                        child: ControlsWidget(
 | 
					 | 
				
			||||||
                          call.room,
 | 
					 | 
				
			||||||
                          call.room.localParticipant!,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                onTap: () {},
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
					              onTap: () {},
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,15 +10,19 @@ import 'package:surface/providers/channel.dart';
 | 
				
			|||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.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';
 | 
				
			||||||
 | 
					import 'package:surface/types/account.dart';
 | 
				
			||||||
import 'package:surface/types/chat.dart';
 | 
					import 'package:surface/types/chat.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/account/account_select.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
					import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChannelDetailScreen extends StatefulWidget {
 | 
					class ChannelDetailScreen extends StatefulWidget {
 | 
				
			||||||
  final String scope;
 | 
					  final String scope;
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ChannelDetailScreen({
 | 
					  const ChannelDetailScreen({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.scope,
 | 
					    required this.scope,
 | 
				
			||||||
@@ -54,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final sn = context.read<SnNetworkProvider>();
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
      final resp = await sn.client
 | 
					      final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
 | 
				
			||||||
          .get('/cgi/im/channels/${_channel!.keyPath}/members/me');
 | 
					 | 
				
			||||||
      _profile = SnChannelMember.fromJson(resp.data);
 | 
					      _profile = SnChannelMember.fromJson(resp.data);
 | 
				
			||||||
      _notifyLevel = _profile!.notify;
 | 
					      _notifyLevel = _profile!.notify;
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
@@ -142,6 +145,25 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _addMember(SnAccount related) async {
 | 
				
			||||||
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      await sn.client.post(
 | 
				
			||||||
 | 
					        '/cgi/im/channels/${_channel!.keyPath}/members',
 | 
				
			||||||
 | 
					        data: {'related': related.name},
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showSnackbar('channelMemberAdded'.tr());
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _showChannelProfileDetail() {
 | 
					  void _showChannelProfileDetail() {
 | 
				
			||||||
    showDialog(
 | 
					    showDialog(
 | 
				
			||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
@@ -165,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _showMemberAdd() {
 | 
					  void _showMemberAdd() async {
 | 
				
			||||||
    showModalBottomSheet(
 | 
					    final user = await showModalBottomSheet<SnAccount?>(
 | 
				
			||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      builder: (context) => _NewChannelMemberWidget(
 | 
					      builder: (context) => AccountSelect(
 | 
				
			||||||
        channel: _channel!,
 | 
					        title: 'channelMemberAdd'.tr(),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					    if (user == null) return;
 | 
				
			||||||
 | 
					    _addMember(user);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -189,7 +214,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
 | 
					    final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
 | 
					        title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
@@ -220,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
              Column(
 | 
					              Column(
 | 
				
			||||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  Text('channelDetailPersonalRegion')
 | 
					                  Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | 
				
			||||||
                      .bold()
 | 
					 | 
				
			||||||
                      .fontSize(17)
 | 
					 | 
				
			||||||
                      .tr()
 | 
					 | 
				
			||||||
                      .padding(horizontal: 20, bottom: 4),
 | 
					 | 
				
			||||||
                  ListTile(
 | 
					                  ListTile(
 | 
				
			||||||
                    leading: const Icon(Symbols.notifications),
 | 
					                    leading: const Icon(Symbols.notifications),
 | 
				
			||||||
                    trailing: DropdownButtonHideUnderline(
 | 
					                    trailing: DropdownButtonHideUnderline(
 | 
				
			||||||
@@ -263,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  ListTile(
 | 
					                  ListTile(
 | 
				
			||||||
                    leading: AccountImage(
 | 
					                    leading: AccountImage(
 | 
				
			||||||
                      content:
 | 
					                      content: ud.getAccountFromCache(_profile!.accountId)?.avatar,
 | 
				
			||||||
                          ud.getAccountFromCache(_profile!.accountId)?.avatar,
 | 
					 | 
				
			||||||
                      radius: 18,
 | 
					                      radius: 18,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    trailing: const Icon(Symbols.chevron_right),
 | 
					                    trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
@@ -283,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
                      trailing: const Icon(Symbols.chevron_right),
 | 
					                      trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
                      title: Text('channelActionLeave').tr(),
 | 
					                      title: Text('channelActionLeave').tr(),
 | 
				
			||||||
                      subtitle: Text('channelActionLeaveDescription').tr(),
 | 
					                      subtitle: Text('channelActionLeaveDescription').tr(),
 | 
				
			||||||
                      contentPadding:
 | 
					                      contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
                          const EdgeInsets.symmetric(horizontal: 24),
 | 
					 | 
				
			||||||
                      onTap: _leaveChannel,
 | 
					                      onTap: _leaveChannel,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
@@ -292,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
            Column(
 | 
					            Column(
 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                Text('channelDetailMemberRegion')
 | 
					                Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | 
				
			||||||
                    .bold()
 | 
					 | 
				
			||||||
                    .fontSize(17)
 | 
					 | 
				
			||||||
                    .tr()
 | 
					 | 
				
			||||||
                    .padding(horizontal: 20, bottom: 4),
 | 
					 | 
				
			||||||
                ListTile(
 | 
					                ListTile(
 | 
				
			||||||
                  leading: const Icon(Symbols.group),
 | 
					                  leading: const Icon(Symbols.group),
 | 
				
			||||||
                  trailing: const Icon(Symbols.chevron_right),
 | 
					                  trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
@@ -318,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
            Column(
 | 
					            Column(
 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                Text('channelDetailAdminRegion')
 | 
					                Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | 
				
			||||||
                    .bold()
 | 
					 | 
				
			||||||
                    .fontSize(17)
 | 
					 | 
				
			||||||
                    .tr()
 | 
					 | 
				
			||||||
                    .padding(horizontal: 20, bottom: 4),
 | 
					 | 
				
			||||||
                ListTile(
 | 
					                ListTile(
 | 
				
			||||||
                  leading: const Icon(Symbols.edit),
 | 
					                  leading: const Icon(Symbols.edit),
 | 
				
			||||||
                  trailing: const Icon(Symbols.chevron_right),
 | 
					                  trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
@@ -361,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
class _ChannelProfileDetailDialog extends StatefulWidget {
 | 
					class _ChannelProfileDetailDialog extends StatefulWidget {
 | 
				
			||||||
  final SnChannel channel;
 | 
					  final SnChannel channel;
 | 
				
			||||||
  final SnChannelMember current;
 | 
					  final SnChannelMember current;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _ChannelProfileDetailDialog({
 | 
					  const _ChannelProfileDetailDialog({
 | 
				
			||||||
    required this.channel,
 | 
					    required this.channel,
 | 
				
			||||||
    required this.current,
 | 
					    required this.current,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<_ChannelProfileDetailDialog> createState() =>
 | 
					  State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState();
 | 
				
			||||||
      _ChannelProfileDetailDialogState();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _ChannelProfileDetailDialogState
 | 
					class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> {
 | 
				
			||||||
    extends State<_ChannelProfileDetailDialog> {
 | 
					 | 
				
			||||||
  bool _isBusy = false;
 | 
					  bool _isBusy = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final TextEditingController _nickController = TextEditingController();
 | 
					  final TextEditingController _nickController = TextEditingController();
 | 
				
			||||||
@@ -443,11 +453,11 @@ class _ChannelProfileDetailDialogState
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class _ChannelMemberListWidget extends StatefulWidget {
 | 
					class _ChannelMemberListWidget extends StatefulWidget {
 | 
				
			||||||
  final SnChannel channel;
 | 
					  final SnChannel channel;
 | 
				
			||||||
  const _ChannelMemberListWidget({super.key, required this.channel});
 | 
					
 | 
				
			||||||
 | 
					  const _ChannelMemberListWidget({required this.channel});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<_ChannelMemberListWidget> createState() =>
 | 
					  State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
 | 
				
			||||||
      _ChannelMemberListWidgetState();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
					class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
				
			||||||
@@ -462,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final ud = context.read<UserDirectoryProvider>();
 | 
					      final ud = context.read<UserDirectoryProvider>();
 | 
				
			||||||
      final sn = context.read<SnNetworkProvider>();
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
      final resp = await sn.client.get(
 | 
					      final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
 | 
				
			||||||
          '/cgi/im/channels/${widget.channel.keyPath}/members',
 | 
					        'take': 10,
 | 
				
			||||||
          queryParameters: {
 | 
					        'offset': 0,
 | 
				
			||||||
            'take': 10,
 | 
					      });
 | 
				
			||||||
            'offset': 0,
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      final out = List<SnChannelMember>.from(
 | 
					      final out = List<SnChannelMember>.from(
 | 
				
			||||||
        resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
 | 
					        resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -525,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
				
			|||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            const Icon(Symbols.group, size: 24),
 | 
					            const Icon(Symbols.group, size: 24),
 | 
				
			||||||
            const Gap(16),
 | 
					            const Gap(16),
 | 
				
			||||||
            Text('channelMemberManage')
 | 
					            Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
				
			||||||
                .tr()
 | 
					 | 
				
			||||||
                .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
					 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
					        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
        Expanded(
 | 
					        Expanded(
 | 
				
			||||||
@@ -538,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            child: InfiniteList(
 | 
					            child: InfiniteList(
 | 
				
			||||||
              itemCount: _members.length,
 | 
					              itemCount: _members.length,
 | 
				
			||||||
              hasReachedMax:
 | 
					              hasReachedMax: _totalCount != null && _members.length >= _totalCount!,
 | 
				
			||||||
                  _totalCount != null && _members.length >= _totalCount!,
 | 
					 | 
				
			||||||
              isLoading: _isBusy,
 | 
					              isLoading: _isBusy,
 | 
				
			||||||
              onFetchData: _fetchMembers,
 | 
					              onFetchData: _fetchMembers,
 | 
				
			||||||
              itemBuilder: (context, index) {
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
@@ -550,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
				
			|||||||
                    content: ud.getAccountFromCache(member.accountId)?.avatar,
 | 
					                    content: ud.getAccountFromCache(member.accountId)?.avatar,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  title: Text(
 | 
					                  title: Text(
 | 
				
			||||||
                    ud.getAccountFromCache(member.accountId)?.name ??
 | 
					                    ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
 | 
				
			||||||
                        'unknown'.tr(),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  subtitle: Text(member.nick ?? 'unknown'.tr()),
 | 
					                  subtitle: Text(member.nick ?? 'unknown'.tr()),
 | 
				
			||||||
                  trailing: SizedBox(
 | 
					                  trailing: SizedBox(
 | 
				
			||||||
@@ -561,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
				
			|||||||
                      mainAxisAlignment: MainAxisAlignment.end,
 | 
					                      mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        IconButton(
 | 
					                        IconButton(
 | 
				
			||||||
                          onPressed:
 | 
					                          onPressed: _isUpdating ? null : () => _deleteMember(member),
 | 
				
			||||||
                              _isUpdating ? null : () => _deleteMember(member),
 | 
					 | 
				
			||||||
                          icon: const Icon(Symbols.person_remove),
 | 
					                          icon: const Icon(Symbols.person_remove),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ],
 | 
					                      ],
 | 
				
			||||||
@@ -577,83 +580,3 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
class _NewChannelMemberWidget extends StatefulWidget {
 | 
					 | 
				
			||||||
  final SnChannel channel;
 | 
					 | 
				
			||||||
  const _NewChannelMemberWidget({super.key, required this.channel});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  State<_NewChannelMemberWidget> createState() =>
 | 
					 | 
				
			||||||
      _NewChannelMemberWidgetState();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _NewChannelMemberWidgetState extends State<_NewChannelMemberWidget> {
 | 
					 | 
				
			||||||
  bool _isBusy = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final TextEditingController _relatedController = TextEditingController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _performAction() async {
 | 
					 | 
				
			||||||
    if (_relatedController.text.isEmpty) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      final sn = context.read<SnNetworkProvider>();
 | 
					 | 
				
			||||||
      await sn.client.post(
 | 
					 | 
				
			||||||
        '/cgi/im/channels/${widget.channel.keyPath}/members',
 | 
					 | 
				
			||||||
        data: {
 | 
					 | 
				
			||||||
          'related': _relatedController.text,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      if (!mounted) return;
 | 
					 | 
				
			||||||
      Navigator.pop(context, true);
 | 
					 | 
				
			||||||
      context.showSnackbar('channelMemberAdded'.tr());
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      if (!mounted) return;
 | 
					 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void dispose() {
 | 
					 | 
				
			||||||
    super.dispose();
 | 
					 | 
				
			||||||
    _relatedController.dispose();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    return StyledWidget(Column(
 | 
					 | 
				
			||||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        Text(
 | 
					 | 
				
			||||||
          'channelMemberAdd',
 | 
					 | 
				
			||||||
          style: Theme.of(context).textTheme.titleLarge,
 | 
					 | 
				
			||||||
        ).tr(),
 | 
					 | 
				
			||||||
        const Gap(12),
 | 
					 | 
				
			||||||
        TextField(
 | 
					 | 
				
			||||||
          controller: _relatedController,
 | 
					 | 
				
			||||||
          readOnly: _isBusy,
 | 
					 | 
				
			||||||
          autocorrect: false,
 | 
					 | 
				
			||||||
          autofocus: true,
 | 
					 | 
				
			||||||
          textCapitalization: TextCapitalization.none,
 | 
					 | 
				
			||||||
          decoration: InputDecoration(
 | 
					 | 
				
			||||||
            labelText: 'fieldMemberRelatedName'.tr(),
 | 
					 | 
				
			||||||
            suffix: SizedBox(
 | 
					 | 
				
			||||||
              height: 24,
 | 
					 | 
				
			||||||
              child: IconButton(
 | 
					 | 
				
			||||||
                onPressed: _isBusy ? null : () => _performAction(),
 | 
					 | 
				
			||||||
                icon: Icon(Symbols.send),
 | 
					 | 
				
			||||||
                visualDensity:
 | 
					 | 
				
			||||||
                    const VisualDensity(horizontal: -4, vertical: -4),
 | 
					 | 
				
			||||||
                padding: EdgeInsets.zero,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    )).padding(all: 24);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.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';
 | 
				
			||||||
@@ -12,10 +13,12 @@ import 'package:surface/types/realm.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';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChatManageScreen extends StatefulWidget {
 | 
					class ChatManageScreen extends StatefulWidget {
 | 
				
			||||||
  final String? editingChannelAlias;
 | 
					  final String? editingChannelAlias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ChatManageScreen({super.key, this.editingChannelAlias});
 | 
					  const ChatManageScreen({super.key, this.editingChannelAlias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -32,6 +35,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
  List<SnRealm>? _realms;
 | 
					  List<SnRealm>? _realms;
 | 
				
			||||||
  SnRealm? _belongToRealm;
 | 
					  SnRealm? _belongToRealm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SnChannel? _editingChannel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _fetchRealms() async {
 | 
					  Future<void> _fetchRealms() async {
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -40,6 +45,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
      _realms = List<SnRealm>.from(
 | 
					      _realms = List<SnRealm>.from(
 | 
				
			||||||
        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
					        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					      if (_editingChannel != null) {
 | 
				
			||||||
 | 
					        _belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (mounted) context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
@@ -47,8 +55,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SnChannel? _editingChannel;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _fetchChannel() async {
 | 
					  Future<void> _fetchChannel() async {
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -87,7 +93,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final resp = await sn.client.request(
 | 
					      final resp = await sn.client.request(
 | 
				
			||||||
        widget.editingChannelAlias != null
 | 
					        widget.editingChannelAlias != null
 | 
				
			||||||
            ? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
 | 
					            ? '/cgi/im/channels/$scope/${_editingChannel!.id}'
 | 
				
			||||||
            : '/cgi/im/channels/$scope',
 | 
					            : '/cgi/im/channels/$scope',
 | 
				
			||||||
        data: payload,
 | 
					        data: payload,
 | 
				
			||||||
        options: Options(
 | 
					        options: Options(
 | 
				
			||||||
@@ -121,11 +127,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: widget.editingChannelAlias != null
 | 
					        title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
 | 
				
			||||||
            ? Text('screenChatManage').tr()
 | 
					 | 
				
			||||||
            : Text('screenChatNew').tr(),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
@@ -137,8 +141,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
					                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
				
			||||||
                dividerColor: Colors.transparent,
 | 
					                dividerColor: Colors.transparent,
 | 
				
			||||||
                content: Text(
 | 
					                content: Text(
 | 
				
			||||||
                  'channelEditingNotice'
 | 
					                  'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
 | 
				
			||||||
                      .tr(args: ['#${_editingChannel!.alias}']),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                actions: [
 | 
					                actions: [
 | 
				
			||||||
                  TextButton(
 | 
					                  TextButton(
 | 
				
			||||||
@@ -161,6 +164,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                items: [
 | 
					                items: [
 | 
				
			||||||
                  ...(_realms?.map(
 | 
					                  ...(_realms?.map(
 | 
				
			||||||
                        (SnRealm item) => DropdownMenuItem<SnRealm>(
 | 
					                        (SnRealm item) => DropdownMenuItem<SnRealm>(
 | 
				
			||||||
 | 
					                          enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
 | 
				
			||||||
                          value: item,
 | 
					                          value: item,
 | 
				
			||||||
                          child: Row(
 | 
					                          child: Row(
 | 
				
			||||||
                            children: [
 | 
					                            children: [
 | 
				
			||||||
@@ -178,15 +182,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                                  mainAxisSize: MainAxisSize.min,
 | 
					                                  mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                                  children: [
 | 
					                                  children: [
 | 
				
			||||||
                                    Text(item.name).textStyle(Theme.of(context)
 | 
					                                    Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
				
			||||||
                                        .textTheme
 | 
					 | 
				
			||||||
                                        .bodyMedium!),
 | 
					 | 
				
			||||||
                                    Text(
 | 
					                                    Text(
 | 
				
			||||||
                                      item.description,
 | 
					                                      item.description,
 | 
				
			||||||
                                      maxLines: 1,
 | 
					                                      maxLines: 1,
 | 
				
			||||||
                                      overflow: TextOverflow.ellipsis,
 | 
					                                      overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                                    ).textStyle(
 | 
					                                    ).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
                                        Theme.of(context).textTheme.bodySmall!),
 | 
					 | 
				
			||||||
                                  ],
 | 
					                                  ],
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
@@ -196,14 +197,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                      ) ??
 | 
					                      ) ??
 | 
				
			||||||
                      []),
 | 
					                      []),
 | 
				
			||||||
                  DropdownMenuItem<SnRealm>(
 | 
					                  DropdownMenuItem<SnRealm>(
 | 
				
			||||||
 | 
					                    enabled: _editingChannel == null,
 | 
				
			||||||
                    value: null,
 | 
					                    value: null,
 | 
				
			||||||
                    child: Row(
 | 
					                    child: Row(
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        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.clear),
 | 
					                          child: const Icon(Symbols.clear),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        const Gap(12),
 | 
					                        const Gap(12),
 | 
				
			||||||
@@ -212,9 +213,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                            mainAxisSize: MainAxisSize.min,
 | 
					                            mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                            children: [
 | 
					                            children: [
 | 
				
			||||||
                              Text('fieldChatBelongToRealmUnset')
 | 
					                              Text('fieldChatBelongToRealmUnset').tr().textStyle(
 | 
				
			||||||
                                  .tr()
 | 
					 | 
				
			||||||
                                  .textStyle(
 | 
					 | 
				
			||||||
                                    Theme.of(context).textTheme.bodyMedium!,
 | 
					                                    Theme.of(context).textTheme.bodyMedium!,
 | 
				
			||||||
                                  ),
 | 
					                                  ),
 | 
				
			||||||
                            ],
 | 
					                            ],
 | 
				
			||||||
@@ -230,10 +229,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                buttonStyleData: const ButtonStyleData(
 | 
					                buttonStyleData: const ButtonStyleData(
 | 
				
			||||||
                  padding: EdgeInsets.only(right: 16),
 | 
					                  padding: EdgeInsets.only(right: 16),
 | 
				
			||||||
                  height: 60,
 | 
					                  height: 48,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                menuItemStyleData: const MenuItemStyleData(
 | 
					                menuItemStyleData: const MenuItemStyleData(
 | 
				
			||||||
                  height: 60,
 | 
					                  height: 48,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -249,8 +248,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    helperText: 'fieldChatAliasHint'.tr(),
 | 
					                    helperText: 'fieldChatAliasHint'.tr(),
 | 
				
			||||||
                    helperMaxLines: 2,
 | 
					                    helperMaxLines: 2,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(4),
 | 
					                const Gap(4),
 | 
				
			||||||
                TextField(
 | 
					                TextField(
 | 
				
			||||||
@@ -259,8 +257,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    border: const UnderlineInputBorder(),
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                    labelText: 'fieldChatName'.tr(),
 | 
					                    labelText: 'fieldChatName'.tr(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(4),
 | 
					                const Gap(4),
 | 
				
			||||||
                TextField(
 | 
					                TextField(
 | 
				
			||||||
@@ -271,8 +268,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    border: const UnderlineInputBorder(),
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                    labelText: 'fieldChatDescription'.tr(),
 | 
					                    labelText: 'fieldChatDescription'.tr(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(12),
 | 
					                const Gap(12),
 | 
				
			||||||
                Row(
 | 
					                Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:developer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
@@ -9,26 +10,36 @@ 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/chat_message_controller.dart';
 | 
					import 'package:surface/controllers/chat_message_controller.dart';
 | 
				
			||||||
 | 
					import 'package:surface/controllers/post_write_controller.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/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/user_directory.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/providers/websocket.dart';
 | 
					import 'package:surface/providers/websocket.dart';
 | 
				
			||||||
import 'package:surface/types/chat.dart';
 | 
					import 'package:surface/types/chat.dart';
 | 
				
			||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
 | 
					import 'package:surface/widgets/chat/call/call_prejoin.dart';
 | 
				
			||||||
import 'package:surface/widgets/chat/chat_message.dart';
 | 
					import 'package:surface/widgets/chat/chat_message.dart';
 | 
				
			||||||
import 'package:surface/widgets/chat/chat_message_input.dart';
 | 
					import 'package:surface/widgets/chat/chat_message_input.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/chat/chat_typing_indicator.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
					import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../../providers/user_directory.dart';
 | 
					class ChatRoomScreenExtra {
 | 
				
			||||||
import '../../providers/userinfo.dart';
 | 
					  final String? initialText;
 | 
				
			||||||
 | 
					  final List<PostWriteMedia>? initialAttachments;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ChatRoomScreenExtra({this.initialText, this.initialAttachments});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChatRoomScreen extends StatefulWidget {
 | 
					class ChatRoomScreen extends StatefulWidget {
 | 
				
			||||||
  final String scope;
 | 
					  final String scope;
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					  final ChatRoomScreenExtra? extra;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ChatRoomScreen({super.key, required this.scope, required this.alias});
 | 
					  const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<ChatRoomScreen> createState() => _ChatRoomScreenState();
 | 
					  State<ChatRoomScreen> createState() => _ChatRoomScreenState();
 | 
				
			||||||
@@ -97,7 +108,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
      print((err as DioException).response?.data);
 | 
					 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setState(() => _isCalling = false);
 | 
					      setState(() => _isCalling = false);
 | 
				
			||||||
@@ -158,7 +168,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,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -176,12 +186,27 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
    _messageController = ChatMessageController(context);
 | 
					    _messageController = ChatMessageController(context);
 | 
				
			||||||
    _fetchChannel().then((_) async {
 | 
					    _fetchChannel().then((_) async {
 | 
				
			||||||
      await _messageController.initialize(_channel!);
 | 
					      await _messageController.initialize(_channel!);
 | 
				
			||||||
      await _messageController.checkUpdate();
 | 
					
 | 
				
			||||||
      await _fetchOngoingCall();
 | 
					      if (widget.extra != null) {
 | 
				
			||||||
 | 
					        WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
				
			||||||
 | 
					          log('[ChatInput] Setting initial text and attachments...');
 | 
				
			||||||
 | 
					          if (widget.extra!.initialText != null) {
 | 
				
			||||||
 | 
					            _inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (widget.extra!.initialAttachments != null) {
 | 
				
			||||||
 | 
					            _inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await Future.wait([
 | 
				
			||||||
 | 
					        _messageController.checkUpdate(),
 | 
				
			||||||
 | 
					        _fetchOngoingCall(),
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final ws = context.read<WebSocketProvider>();
 | 
					    final ws = context.read<WebSocketProvider>();
 | 
				
			||||||
    _wsSubscription = ws.stream.stream.listen((event) {
 | 
					    _wsSubscription = ws.pk.stream.listen((event) {
 | 
				
			||||||
      switch (event.method) {
 | 
					      switch (event.method) {
 | 
				
			||||||
        case 'calls.new':
 | 
					        case 'calls.new':
 | 
				
			||||||
          final payload = SnChatCall.fromJson(event.payload!);
 | 
					          final payload = SnChatCall.fromJson(event.payload!);
 | 
				
			||||||
@@ -211,7 +236,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
    final call = context.watch<ChatCallProvider>();
 | 
					    final call = context.watch<ChatCallProvider>();
 | 
				
			||||||
    final ud = context.read<UserDirectoryProvider>();
 | 
					    final ud = context.read<UserDirectoryProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: Text(
 | 
					        title: Text(
 | 
				
			||||||
          _channel?.type == 1
 | 
					          _channel?.type == 1
 | 
				
			||||||
@@ -281,11 +306,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
                Expanded(
 | 
					                Expanded(
 | 
				
			||||||
                  child: InfiniteList(
 | 
					                  child: InfiniteList(
 | 
				
			||||||
                    reverse: true,
 | 
					                    reverse: true,
 | 
				
			||||||
                    padding: const EdgeInsets.only(
 | 
					                    padding: const EdgeInsets.only(top: 12),
 | 
				
			||||||
                      left: 12,
 | 
					 | 
				
			||||||
                      right: 12,
 | 
					 | 
				
			||||||
                      top: 12,
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    hasReachedMax: _messageController.isAllLoaded,
 | 
					                    hasReachedMax: _messageController.isAllLoaded,
 | 
				
			||||||
                    itemCount: _messageController.messages.length,
 | 
					                    itemCount: _messageController.messages.length,
 | 
				
			||||||
                    isLoading: _messageController.isLoading,
 | 
					                    isLoading: _messageController.isLoading,
 | 
				
			||||||
@@ -311,23 +332,20 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                      return Align(
 | 
					                      return Align(
 | 
				
			||||||
                        alignment: Alignment.centerLeft,
 | 
					                        alignment: Alignment.centerLeft,
 | 
				
			||||||
                        child: Container(
 | 
					                        child: ChatMessage(
 | 
				
			||||||
                          constraints: BoxConstraints(maxWidth: 480),
 | 
					                          data: message,
 | 
				
			||||||
                          child: ChatMessage(
 | 
					                          isMerged: canMerge,
 | 
				
			||||||
                            data: message,
 | 
					                          hasMerged: canMergePrevious,
 | 
				
			||||||
                            isMerged: canMerge,
 | 
					                          isPending: _messageController.unconfirmedMessages.contains(message.uuid),
 | 
				
			||||||
                            hasMerged: canMergePrevious,
 | 
					                          onReply: (value) {
 | 
				
			||||||
                            isPending: _messageController.unconfirmedMessages.contains(message.uuid),
 | 
					                            _inputGlobalKey.currentState?.setReply(value);
 | 
				
			||||||
                            onReply: (value) {
 | 
					                          },
 | 
				
			||||||
                              _inputGlobalKey.currentState?.setReply(value);
 | 
					                          onEdit: (value) {
 | 
				
			||||||
                            },
 | 
					                            _inputGlobalKey.currentState?.setEdit(value);
 | 
				
			||||||
                            onEdit: (value) {
 | 
					                          },
 | 
				
			||||||
                              _inputGlobalKey.currentState?.setEdit(value);
 | 
					                          onDelete: (value) {
 | 
				
			||||||
                            },
 | 
					                            _inputGlobalKey.currentState?.deleteMessage(value);
 | 
				
			||||||
                            onDelete: (value) {
 | 
					                          },
 | 
				
			||||||
                              _inputGlobalKey.currentState?.deleteMessage(value);
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
@@ -336,11 +354,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
              if (!_messageController.isPending)
 | 
					              if (!_messageController.isPending)
 | 
				
			||||||
                Material(
 | 
					                Material(
 | 
				
			||||||
                  elevation: 2,
 | 
					                  elevation: 2,
 | 
				
			||||||
                  child: ChatMessageInput(
 | 
					                  child: Column(
 | 
				
			||||||
                    key: _inputGlobalKey,
 | 
					                    children: [
 | 
				
			||||||
                    otherMember: _otherMember,
 | 
					                      ChatTypingIndicator(controller: _messageController),
 | 
				
			||||||
                    controller: _messageController,
 | 
					                      ChatMessageInput(
 | 
				
			||||||
                  ).padding(bottom: MediaQuery.of(context).padding.bottom),
 | 
					                        key: _inputGlobalKey,
 | 
				
			||||||
 | 
					                        otherMember: _otherMember,
 | 
				
			||||||
 | 
					                        controller: _messageController,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      Gap(MediaQuery.of(context).padding.bottom),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,12 +5,29 @@ 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/navigation/app_scaffold.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 +41,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,15 +79,22 @@ 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
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      floatingActionButtonLocation: ExpandableFab.location,
 | 
					      floatingActionButtonLocation: ExpandableFab.location,
 | 
				
			||||||
      floatingActionButton: ExpandableFab(
 | 
					      floatingActionButton: ExpandableFab(
 | 
				
			||||||
        key: _fabKey,
 | 
					        key: _fabKey,
 | 
				
			||||||
@@ -59,27 +102,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 +131,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 +152,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();
 | 
				
			||||||
@@ -127,14 +161,53 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					          Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Text('writePostTypeQuestion').tr(),
 | 
				
			||||||
 | 
					              const Gap(20),
 | 
				
			||||||
 | 
					              FloatingActionButton(
 | 
				
			||||||
 | 
					                heroTag: null,
 | 
				
			||||||
 | 
					                tooltip: 'writePostTypeQuestion'.tr(),
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
				
			||||||
 | 
					                    'mode': 'questions',
 | 
				
			||||||
 | 
					                  }).then((value) {
 | 
				
			||||||
 | 
					                    if (value == true) {
 | 
				
			||||||
 | 
					                      _refreshPosts();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  _fabKey.currentState!.toggle();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                child: const Icon(Symbols.question_answer),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Text('writePostTypeVideo').tr(),
 | 
				
			||||||
 | 
					              const Gap(20),
 | 
				
			||||||
 | 
					              FloatingActionButton(
 | 
				
			||||||
 | 
					                heroTag: null,
 | 
				
			||||||
 | 
					                tooltip: 'writePostTypeVideo'.tr(),
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
				
			||||||
 | 
					                    'mode': 'videos',
 | 
				
			||||||
 | 
					                  }).then((value) {
 | 
				
			||||||
 | 
					                    if (value == true) {
 | 
				
			||||||
 | 
					                      _refreshPosts();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  _fabKey.currentState!.toggle();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                child: const Icon(Symbols.video_call),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      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,7 +224,36 @@ 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(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            const SliverGap(12),
 | 
				
			||||||
            SliverInfiniteList(
 | 
					            SliverInfiniteList(
 | 
				
			||||||
              itemCount: _posts.length,
 | 
					              itemCount: _posts.length,
 | 
				
			||||||
              isLoading: _isBusy,
 | 
					              isLoading: _isBusy,
 | 
				
			||||||
@@ -159,28 +261,18 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
              hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
					              hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
				
			||||||
              onFetchData: _fetchPosts,
 | 
					              onFetchData: _fetchPosts,
 | 
				
			||||||
              itemBuilder: (context, idx) {
 | 
					              itemBuilder: (context, idx) {
 | 
				
			||||||
                return GestureDetector(
 | 
					                return OpenablePostItem(
 | 
				
			||||||
                  child: PostItem(
 | 
					                  data: _posts[idx],
 | 
				
			||||||
                    data: _posts[idx],
 | 
					                  maxWidth: 640,
 | 
				
			||||||
                    maxWidth: 640,
 | 
					                  onChanged: (data) {
 | 
				
			||||||
                    onChanged: (data) {
 | 
					                    setState(() => _posts[idx] = data);
 | 
				
			||||||
                      setState(() => _posts[idx] = data);
 | 
					                  },
 | 
				
			||||||
                    },
 | 
					                  onDeleted: () {
 | 
				
			||||||
                    onDeleted: () {
 | 
					                    _refreshPosts();
 | 
				
			||||||
                      _posts.clear();
 | 
					 | 
				
			||||||
                      _fetchPosts();
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  onTap: () {
 | 
					 | 
				
			||||||
                    GoRouter.of(context).pushNamed(
 | 
					 | 
				
			||||||
                      'postDetail',
 | 
					 | 
				
			||||||
                      pathParameters: {'slug': _posts[idx].id.toString()},
 | 
					 | 
				
			||||||
                      extra: _posts[idx],
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
              separatorBuilder: (context, index) => const Divider(height: 1),
 | 
					              separatorBuilder: (_, __) => const Gap(8),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,14 +6,15 @@ import 'package:provider/provider.dart';
 | 
				
			|||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.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/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/types/account.dart';
 | 
					import 'package:surface/types/account.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/account/account_select.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/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import '../providers/userinfo.dart';
 | 
					import 'package:surface/widgets/unauthorized_hint.dart';
 | 
				
			||||||
import '../widgets/unauthorized_hint.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const kFriendStatus = {
 | 
					const kFriendStatus = {
 | 
				
			||||||
  0: 'friendStatusPending',
 | 
					  0: 'friendStatusPending',
 | 
				
			||||||
@@ -167,6 +168,24 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _sendRequest(SnAccount user) async {
 | 
				
			||||||
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      await sn.client.post('/cgi/id/users/me/relations', data: {
 | 
				
			||||||
 | 
					        'related': user.name,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showSnackbar('friendRequestSent'.tr());
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
@@ -180,7 +199,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
    final ua = context.read<UserProvider>();
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!ua.isAuthorized) {
 | 
					    if (!ua.isAuthorized) {
 | 
				
			||||||
      return Scaffold(
 | 
					      return AppScaffold(
 | 
				
			||||||
        appBar: AppBar(
 | 
					        appBar: AppBar(
 | 
				
			||||||
          leading: AutoAppBarLeading(),
 | 
					          leading: AutoAppBarLeading(),
 | 
				
			||||||
          title: Text('screenFriend').tr(),
 | 
					          title: Text('screenFriend').tr(),
 | 
				
			||||||
@@ -191,18 +210,23 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text('screenFriend').tr(),
 | 
					        title: Text('screenFriend').tr(),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      floatingActionButton: FloatingActionButton(
 | 
					      floatingActionButton: FloatingActionButton(
 | 
				
			||||||
        child: const Icon(Symbols.add),
 | 
					        child: const Icon(Symbols.add),
 | 
				
			||||||
        onPressed: () {
 | 
					        onPressed: () async {
 | 
				
			||||||
          showModalBottomSheet(
 | 
					          final user = await showModalBottomSheet<SnAccount?>(
 | 
				
			||||||
            context: context,
 | 
					            context: context,
 | 
				
			||||||
            builder: (context) => _NewFriendWidget(),
 | 
					            builder: (context) => AccountSelect(
 | 
				
			||||||
 | 
					              title: 'friendNew'.tr(),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					          if (!mounted) return;
 | 
				
			||||||
 | 
					          if (user == null) return;
 | 
				
			||||||
 | 
					          _sendRequest(user);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: Column(
 | 
					      body: Column(
 | 
				
			||||||
@@ -230,55 +254,54 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
              trailing: const Icon(Symbols.chevron_right),
 | 
					              trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
              onTap: _showBlocks,
 | 
					              onTap: _showBlocks,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          if (_requests.isNotEmpty || _blocks.isNotEmpty)
 | 
					          if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
 | 
				
			||||||
            const Divider(height: 1),
 | 
					 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: RefreshIndicator(
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
              onRefresh: () => Future.wait([
 | 
					              context: context,
 | 
				
			||||||
                _fetchRelations(),
 | 
					              removeTop: true,
 | 
				
			||||||
                _fetchRequests(),
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
              ]),
 | 
					                onRefresh: () => Future.wait([
 | 
				
			||||||
              child: ListView.builder(
 | 
					                  _fetchRelations(),
 | 
				
			||||||
                itemCount: _relations.length,
 | 
					                  _fetchRequests(),
 | 
				
			||||||
                itemBuilder: (context, index) {
 | 
					                ]),
 | 
				
			||||||
                  final relation = _relations[index];
 | 
					                child: ListView.builder(
 | 
				
			||||||
                  final other = relation.related;
 | 
					                  itemCount: _relations.length,
 | 
				
			||||||
                  return ListTile(
 | 
					                  itemBuilder: (context, index) {
 | 
				
			||||||
                    contentPadding: const EdgeInsets.only(right: 24, left: 16),
 | 
					                    final relation = _relations[index];
 | 
				
			||||||
                    leading: AccountImage(content: other?.avatar),
 | 
					                    final other = relation.related;
 | 
				
			||||||
                    title: Text(other?.nick ?? 'unknown'),
 | 
					                    return ListTile(
 | 
				
			||||||
                    subtitle: Text(other?.nick ?? 'unknown'),
 | 
					                      contentPadding: const EdgeInsets.only(right: 24, left: 16),
 | 
				
			||||||
                    trailing: SizedBox(
 | 
					                      leading: AccountImage(content: other?.avatar),
 | 
				
			||||||
                      height: 48,
 | 
					                      title: Text(other?.nick ?? 'unknown'),
 | 
				
			||||||
                      width: 120,
 | 
					                      subtitle: Text(other?.nick ?? 'unknown'),
 | 
				
			||||||
                      child: Column(
 | 
					                      trailing: SizedBox(
 | 
				
			||||||
                        mainAxisSize: MainAxisSize.min,
 | 
					                        height: 48,
 | 
				
			||||||
                        mainAxisAlignment: MainAxisAlignment.center,
 | 
					                        width: 120,
 | 
				
			||||||
                        crossAxisAlignment: CrossAxisAlignment.end,
 | 
					                        child: Column(
 | 
				
			||||||
                        children: [
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                          Row(
 | 
					                          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
                            mainAxisAlignment: MainAxisAlignment.end,
 | 
					                          crossAxisAlignment: CrossAxisAlignment.end,
 | 
				
			||||||
                            children: [
 | 
					                          children: [
 | 
				
			||||||
                              InkWell(
 | 
					                            Row(
 | 
				
			||||||
                                onTap: _isUpdating
 | 
					                              mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
                                    ? null
 | 
					                              children: [
 | 
				
			||||||
                                    : () => _changeRelation(relation, 2),
 | 
					                                InkWell(
 | 
				
			||||||
                                child: Text('friendBlock').tr(),
 | 
					                                  onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
 | 
				
			||||||
                              ),
 | 
					                                  child: Text('friendBlock').tr(),
 | 
				
			||||||
                              const Gap(8),
 | 
					                                ),
 | 
				
			||||||
                              InkWell(
 | 
					                                const Gap(8),
 | 
				
			||||||
                                onTap: _isUpdating
 | 
					                                InkWell(
 | 
				
			||||||
                                    ? null
 | 
					                                  onTap: _isUpdating ? null : () => _deleteRelation(relation),
 | 
				
			||||||
                                    : () => _deleteRelation(relation),
 | 
					                                  child: Text('friendDeleteAction').tr(),
 | 
				
			||||||
                                child: Text('friendDeleteAction').tr(),
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                              ],
 | 
				
			||||||
                            ],
 | 
					                            ),
 | 
				
			||||||
                          ),
 | 
					                          ],
 | 
				
			||||||
                        ],
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    );
 | 
				
			||||||
                  );
 | 
					                  },
 | 
				
			||||||
                },
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -288,84 +311,10 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _NewFriendWidget extends StatefulWidget {
 | 
					 | 
				
			||||||
  const _NewFriendWidget({super.key});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  State<_NewFriendWidget> createState() => _NewFriendWidgetState();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _NewFriendWidgetState extends State<_NewFriendWidget> {
 | 
					 | 
				
			||||||
  bool _isBusy = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final TextEditingController _relatedController = TextEditingController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _sendRequest() async {
 | 
					 | 
				
			||||||
    if (_relatedController.text.isEmpty) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      final sn = context.read<SnNetworkProvider>();
 | 
					 | 
				
			||||||
      await sn.client.post('/cgi/id/users/me/relations', data: {
 | 
					 | 
				
			||||||
        'related': _relatedController.text,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      if (!mounted) return;
 | 
					 | 
				
			||||||
      Navigator.pop(context, true);
 | 
					 | 
				
			||||||
      context.showSnackbar('friendRequestSent'.tr());
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      if (!mounted) return;
 | 
					 | 
				
			||||||
      context.showErrorDialog(err);
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void dispose() {
 | 
					 | 
				
			||||||
    super.dispose();
 | 
					 | 
				
			||||||
    _relatedController.dispose();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    return StyledWidget(Column(
 | 
					 | 
				
			||||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        Text(
 | 
					 | 
				
			||||||
          'friendNew',
 | 
					 | 
				
			||||||
          style: Theme.of(context).textTheme.titleLarge,
 | 
					 | 
				
			||||||
        ).tr(),
 | 
					 | 
				
			||||||
        const Gap(12),
 | 
					 | 
				
			||||||
        TextField(
 | 
					 | 
				
			||||||
          controller: _relatedController,
 | 
					 | 
				
			||||||
          readOnly: _isBusy,
 | 
					 | 
				
			||||||
          autocorrect: false,
 | 
					 | 
				
			||||||
          autofocus: true,
 | 
					 | 
				
			||||||
          textCapitalization: TextCapitalization.none,
 | 
					 | 
				
			||||||
          decoration: InputDecoration(
 | 
					 | 
				
			||||||
            labelText: 'fieldFriendRelatedName'.tr(),
 | 
					 | 
				
			||||||
            suffix: SizedBox(
 | 
					 | 
				
			||||||
              height: 24,
 | 
					 | 
				
			||||||
              child: IconButton(
 | 
					 | 
				
			||||||
                onPressed: _isBusy ? null : () => _sendRequest(),
 | 
					 | 
				
			||||||
                icon: Icon(Symbols.send),
 | 
					 | 
				
			||||||
                visualDensity:
 | 
					 | 
				
			||||||
                    const VisualDensity(horizontal: -4, vertical: -4),
 | 
					 | 
				
			||||||
                padding: EdgeInsets.zero,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    )).padding(all: 24);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _FriendshipListWidget extends StatefulWidget {
 | 
					class _FriendshipListWidget extends StatefulWidget {
 | 
				
			||||||
  final List<SnRelationship> relations;
 | 
					  final List<SnRelationship> relations;
 | 
				
			||||||
  const _FriendshipListWidget({super.key, required this.relations});
 | 
					
 | 
				
			||||||
 | 
					  const _FriendshipListWidget({required this.relations});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<_FriendshipListWidget> createState() => _FriendshipListWidgetState();
 | 
					  State<_FriendshipListWidget> createState() => _FriendshipListWidgetState();
 | 
				
			||||||
@@ -471,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
 | 
				
			|||||||
              mainAxisAlignment: MainAxisAlignment.center,
 | 
					              mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.end,
 | 
					              crossAxisAlignment: CrossAxisAlignment.end,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                Text(kFriendStatus[relation.status] ?? 'unknown')
 | 
					                Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
 | 
				
			||||||
                    .tr()
 | 
					 | 
				
			||||||
                    .opacity(0.75),
 | 
					 | 
				
			||||||
                if (relation.status == 0)
 | 
					                if (relation.status == 0)
 | 
				
			||||||
                  Row(
 | 
					                  Row(
 | 
				
			||||||
                    mainAxisAlignment: MainAxisAlignment.end,
 | 
					                    mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
@@ -494,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
 | 
				
			|||||||
                    mainAxisAlignment: MainAxisAlignment.end,
 | 
					                    mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
                    children: [
 | 
					                    children: [
 | 
				
			||||||
                      InkWell(
 | 
					                      InkWell(
 | 
				
			||||||
                        onTap:
 | 
					                        onTap: _isBusy ? null : () => _changeRelation(relation, 1),
 | 
				
			||||||
                            _isBusy ? null : () => _changeRelation(relation, 1),
 | 
					 | 
				
			||||||
                        child: Text('friendUnblock').tr(),
 | 
					                        child: Text('friendUnblock').tr(),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      const Gap(8),
 | 
					                      const Gap(8),
 | 
				
			||||||
 
 | 
				
			|||||||