Compare commits
211 Commits
04b9427cdf
...
2.2.2+59
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
2eb1f4b52b | |||
00678c0ac8 | |||
abc21f858b | |||
d67e33a41d | |||
4daff41b3e | |||
f92418ea4b | |||
89c912a35b | |||
09ad917e5d | |||
5c377dc0b6 | |||
8bdaf05223 | |||
e920bd954c | |||
e395ac87c5 | |||
026a4dfb27 | |||
df18370bde | |||
80a66136ce | |||
1f8d47f6c3 | |||
b750cc3c67 | |||
b618fcc6da | |||
f763c7515a | |||
c7d5cb48ac | |||
39470d7dbf | |||
4328de21ef | |||
a3a0e8c7a2 | |||
210c73a831 | |||
edaeae386e | |||
be66ea354e | |||
d7c1ffe3cc | |||
240ad7dc7e | |||
bb5fe9c380 | |||
1347aacbc5 | |||
8880647360 | |||
717bccbf3f | |||
018441ea0b | |||
336bb88ca4 | |||
811fc40d79 | |||
e05209ba3c | |||
623095473e | |||
f47f1b175a | |||
3b1d291037 | |||
2abc9808e2 | |||
41dd7d0b64 | |||
20f4e780bc | |||
da43c940f2 | |||
a9ca8d36bc | |||
1980843ac0 | |||
96f6752bbe |
@ -1,12 +1,12 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "solian-next",
|
||||
"region": "solian",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "solian-next",
|
||||
"site": "solian-next-web",
|
||||
"region": "solian",
|
||||
"site": "solian-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"
|
||||
errors:
|
||||
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:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
|
@ -9,14 +9,39 @@ plugins {
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.glance:glance:1.1.1'
|
||||
implementation 'androidx.glance:glance-appwidget:1.1.1'
|
||||
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'io.coil-kt.coil3:coil-compose:3.0.4'
|
||||
implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.4'
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
|
||||
namespace = "dev.solsynth.solian"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.8"
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
@ -24,21 +49,32 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "dev.solsynth.solian"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdk = 26
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
signingConfigs {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.debug
|
||||
keyAlias = keystoreProperties['keyAlias']
|
||||
keyPassword = keystoreProperties['keyPassword']
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,21 +11,59 @@
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:label="Solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Widgets Indents -->
|
||||
<intent-filter>
|
||||
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Sharing Intents -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
@ -44,7 +82,30 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<!-- Widgets -->
|
||||
<receiver android:name=".widgets.CheckInWidgetReceiver"
|
||||
android:label="Check In"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/check_in_widget" />
|
||||
</receiver>
|
||||
<receiver android:name=".widgets.RandomPostWidgetReceiver"
|
||||
android:label="Random Post"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/random_post_widget" />
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
@ -0,0 +1,6 @@
|
||||
package dev.solsynth.solian.data
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
data class SolarPagination<T>(val count: Int, val data: List<T>)
|
@ -0,0 +1,35 @@
|
||||
package dev.solsynth.solian.data
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import java.time.Instant
|
||||
|
||||
@Keep
|
||||
data class SolarPost(
|
||||
val id: Int,
|
||||
val body: SolarPostBody,
|
||||
val publisher: SolarPublisher,
|
||||
val publisherId: Int,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
val editedAt: Instant?,
|
||||
val publishedAt: Instant?
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class SolarPostBody(
|
||||
val content: String?,
|
||||
val title: String?,
|
||||
val description: String?,
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class SolarPublisher(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val nick: String,
|
||||
val description: String?,
|
||||
val avatar: String?,
|
||||
val banner: String?,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant
|
||||
)
|
@ -0,0 +1,38 @@
|
||||
package dev.solsynth.solian.data
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import java.lang.reflect.Type
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Keep
|
||||
class InstantAdapter : JsonSerializer<Instant?>,
|
||||
JsonDeserializer<Instant?> {
|
||||
override fun serialize(
|
||||
src: Instant?,
|
||||
typeOfSrc: Type?,
|
||||
context: JsonSerializationContext?
|
||||
): JsonElement {
|
||||
return JsonPrimitive(formatter.format(src))
|
||||
}
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): Instant {
|
||||
return Instant.parse(json.asString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.solsynth.solian.data
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import java.time.Instant
|
||||
|
||||
@Keep
|
||||
data class SolarUser(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val nick: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class SolarCheckInRecord(
|
||||
val id: Int,
|
||||
val resultTier: Int,
|
||||
val resultExperience: Int,
|
||||
val createdAt: Instant
|
||||
)
|
@ -0,0 +1,128 @@
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.state.GlanceStateDefinition
|
||||
import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import com.google.gson.FieldNamingPolicy
|
||||
import com.google.gson.GsonBuilder
|
||||
import dev.solsynth.solian.MainActivity
|
||||
import dev.solsynth.solian.data.InstantAdapter
|
||||
import dev.solsynth.solian.data.SolarCheckInRecord
|
||||
import es.antonborri.home_widget.actionStartActivity
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class CheckInWidget : GlanceAppWidget() {
|
||||
override val stateDefinition: GlanceStateDefinition<*>?
|
||||
get() = HomeWidgetGlanceStateDefinition()
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
provideContent {
|
||||
GlanceTheme {
|
||||
GlanceContent(context, currentState())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
|
||||
val gson =
|
||||
GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.registerTypeAdapter(Instant::class.java, InstantAdapter())
|
||||
.create()
|
||||
val resultTierSymbols = listOf("大凶", "凶", "中平", "吉", "大吉")
|
||||
|
||||
val prefs = currentState.preferences
|
||||
val checkInRaw: String? = prefs.getString("pas_check_in_record", null)
|
||||
|
||||
val checkIn: SolarCheckInRecord? =
|
||||
checkInRaw?.let { checkInRaw ->
|
||||
gson.fromJson(checkInRaw, SolarCheckInRecord::class.java)
|
||||
} ?: null;
|
||||
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.background(GlanceTheme.colors.widgetBackground)
|
||||
.padding(16.dp)
|
||||
.clickable(
|
||||
onClick = actionStartActivity<MainActivity>(
|
||||
context,
|
||||
Uri.parse("https://sn.solsynth.dev")
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (checkIn != null) {
|
||||
val dateFormatter = DateTimeFormatter.ofPattern("EEE, MM/dd")
|
||||
|
||||
val checkDate = checkIn.createdAt.atZone(ZoneId.of("UTC")).toLocalDate()
|
||||
val currentDate = LocalDate.now()
|
||||
if (checkDate.isEqual(currentDate)) {
|
||||
Column {
|
||||
Text(
|
||||
text = resultTierSymbols[checkIn.resultTier],
|
||||
style = TextStyle(
|
||||
fontSize = 17.sp,
|
||||
color = GlanceTheme.colors.onSurface
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "+${checkIn.resultExperience} EXP",
|
||||
style = TextStyle(
|
||||
fontSize = 13.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = GlanceTheme.colors.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = GlanceModifier.height(8.dp))
|
||||
Row(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = OffsetDateTime.ofInstant(
|
||||
checkIn.createdAt,
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
.format(dateFormatter),
|
||||
style = TextStyle(
|
||||
fontSize = 11.sp,
|
||||
color = GlanceTheme.colors.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return@Column;
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "You haven't checked in today",
|
||||
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dev.solsynth.solian.widgets
|
||||
|
||||
import CheckInWidget
|
||||
import HomeWidgetGlanceWidgetReceiver
|
||||
|
||||
class CheckInWidgetReceiver : HomeWidgetGlanceWidgetReceiver<CheckInWidget>() {
|
||||
override val glanceAppWidget = CheckInWidget()
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
import HomeWidgetGlanceState
|
||||
import HomeWidgetGlanceStateDefinition
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.state.GlanceStateDefinition
|
||||
import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import com.google.gson.FieldNamingPolicy
|
||||
import com.google.gson.GsonBuilder
|
||||
import dev.solsynth.solian.MainActivity
|
||||
import dev.solsynth.solian.data.InstantAdapter
|
||||
import dev.solsynth.solian.data.SolarPost
|
||||
import es.antonborri.home_widget.actionStartActivity
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class RandomPostWidget : GlanceAppWidget() {
|
||||
override val stateDefinition: GlanceStateDefinition<*>?
|
||||
get() = HomeWidgetGlanceStateDefinition()
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
provideContent {
|
||||
GlanceTheme {
|
||||
GlanceContent(context, currentState())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GlanceContent(
|
||||
context: Context,
|
||||
currentState: HomeWidgetGlanceState,
|
||||
) {
|
||||
val prefs = currentState.preferences
|
||||
val postRaw = prefs.getString("int_random_post", null)
|
||||
|
||||
val gson =
|
||||
GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.registerTypeAdapter(Instant::class.java, InstantAdapter())
|
||||
.create()
|
||||
|
||||
val data: SolarPost? = postRaw?.let { postRaw ->
|
||||
gson.fromJson(postRaw, SolarPost::class.java)
|
||||
} ?: null;
|
||||
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.background(GlanceTheme.colors.widgetBackground)
|
||||
.padding(16.dp)
|
||||
.clickable(
|
||||
onClick = actionStartActivity<MainActivity>(
|
||||
context,
|
||||
Uri.parse("https://sn.solsynth.dev/posts/${data!!.id}")
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (data != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = data.publisher.nick,
|
||||
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.width(8.dp))
|
||||
Text(
|
||||
text = "@${data.publisher.name}",
|
||||
style = TextStyle(
|
||||
fontSize = 13.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = GlanceTheme.colors.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = GlanceModifier.height(8.dp))
|
||||
|
||||
if (data.body.title != null) {
|
||||
Text(
|
||||
text = data.body.title,
|
||||
style = TextStyle(fontSize = 19.sp, color = GlanceTheme.colors.onSurface)
|
||||
)
|
||||
}
|
||||
if (data.body.description != null) {
|
||||
Text(
|
||||
text = data.body.description,
|
||||
style = TextStyle(fontSize = 17.sp, color = GlanceTheme.colors.onSurface)
|
||||
)
|
||||
}
|
||||
|
||||
if (data.body.title != null || data.body.description != null) {
|
||||
Spacer(modifier = GlanceModifier.height(8.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = data.body.content ?: "No content",
|
||||
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface),
|
||||
)
|
||||
|
||||
Spacer(modifier = GlanceModifier.height(8.dp))
|
||||
|
||||
|
||||
Text(
|
||||
LocalDateTime.ofInstant(data.createdAt, ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
|
||||
style = TextStyle(fontSize = 13.sp, color = GlanceTheme.colors.onSurface),
|
||||
)
|
||||
|
||||
Text(
|
||||
"#${data.id}",
|
||||
style = TextStyle(
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = GlanceTheme.colors.onSurface
|
||||
),
|
||||
)
|
||||
|
||||
return@Column;
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = GlanceModifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.Vertical.CenterVertically,
|
||||
horizontalAlignment = Alignment.Horizontal.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "No Recommendations",
|
||||
style = TextStyle(
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = GlanceTheme.colors.onSurface
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "Open app to load some posts",
|
||||
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dev.solsynth.solian.widgets
|
||||
|
||||
import RandomPostWidget
|
||||
import HomeWidgetGlanceWidgetReceiver
|
||||
|
||||
class RandomPostWidgetReceiver : HomeWidgetGlanceWidgetReceiver<RandomPostWidget>() {
|
||||
override val glanceAppWidget = RandomPostWidget()
|
||||
}
|
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"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFFFF</color>
|
||||
<color name="ic_notification_background">#00000000</color>
|
||||
</resources>
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 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
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
@ -16,7 +16,7 @@
|
||||
running.
|
||||
|
||||
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>
|
||||
</style>
|
||||
</resources>
|
||||
|
7
android/app/src/main/res/xml/check_in_widget.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="40dp"
|
||||
android:minHeight="40dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="10000">
|
||||
</appwidget-provider>
|
7
android/app/src/main/res/xml/random_post_widget.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="240dp"
|
||||
android:minHeight="40dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="10000">
|
||||
</appwidget-provider>
|
14
android/app/src/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
-keepclassmembers class kotlin.Metadata { *; }
|
||||
-keep class dev.solsynth.solian.** { *; }
|
||||
-keep public class dev.solsynth.solian.data.** { public *; }
|
||||
-keepclassmembers class dev.solsynth.solian.data.** { *; }
|
||||
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
-keepattributes EnclosingMethod
|
||||
|
||||
-keep class com.google.gson.** { *; }
|
||||
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
@ -3,6 +3,15 @@ allprojects {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency {
|
||||
if ((requested.group == "androidx.work") && (requested.name.startsWith("work-runtime"))) {
|
||||
useVersion("2.9.1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = "../build"
|
||||
|
@ -18,7 +18,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version '8.7.2' apply false
|
||||
id "com.android.application" version '8.7.3' apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||
id "com.google.firebase.crashlytics" version "2.8.1" apply false
|
||||
|
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": "AteChip",
|
||||
"name": "Cat ate chips",
|
||||
"attachment_id": "d0b692cc64054463",
|
||||
"pack_id": 2
|
||||
}
|
||||
}
|
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
|
||||
}
|
||||
}
|
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
|
||||
}
|
||||
}
|
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
|
||||
atk: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NvbHN5bnRoLmRldiIsInN1YiI6IjEiLCJhdWQiOlsic29sYXItbmV0d29yayJdLCJleHAiOjE3MzgwODE2OTYsIm5iZiI6MTczODA3ODY5NiwiaWF0IjoxNzM4MDc4Njk2LCJqdGkiOiI1Yzg2MTYxZC00MTZjLTQwNDYtOWFlNS04YWZhNGIyZjdlMTkiLCJzZWQiOiIyMTciLCJ0eXAiOiJhY2Nlc3MifQ.LdLZ6FLb8IqPI__U8sT6VyxK5S_ZgwVGGL-tk01tK9C19wnbFFThPDgga1pJu_VVVpGVxzvMyd-3tBotzLMB5LjhYbtPOJakN2oug1HAgJ8zfc1clORlpHlUVisaiQtl3ZkWtzxni8etZDhJpqHU65IGQG01TO6PZGLgxKkMel4gGeeKhHpg9Q4Eewr3Pbl_wJkHVChJ9IJPmgioc_CACE10nEHZgwptCbndUz3AbIDOG9qW-7ZoprtKoRwAcuUXANK277VvdBRhwQjqKBREqVeMXP_Rv37jDPYhWpfS6HtKoHElZOTJG-69S2Zc3HyHlPJAPlzLAjGPoxScky79Gg
|
||||
}
|
||||
vars:secret [
|
||||
third_client_tk
|
||||
]
|
@ -17,6 +17,10 @@
|
||||
"screenAccountProfileEdit": "Edit Profile",
|
||||
"screenAbuseReport": "Abuse Reports",
|
||||
"screenSettings": "Settings",
|
||||
"screenAccountSettings": "Account Settings",
|
||||
"screenFactorSettings": "Auth Factors",
|
||||
"screenAccountWallet": "Wallet",
|
||||
"screenNews": "News",
|
||||
"screenAlbum": "Album",
|
||||
"screenChat": "Chat",
|
||||
"screenChatManage": "Edit Channel",
|
||||
@ -57,7 +61,7 @@
|
||||
"reply": "Reply",
|
||||
"unset": "Unset",
|
||||
"untitled": "Untitled",
|
||||
"postDetail": "Post detail",
|
||||
"postDetail": "Post Detail",
|
||||
"postNoun": "Post",
|
||||
"postReadMore": "Read more",
|
||||
"postReadEstimate": "Est read time {}",
|
||||
@ -103,8 +107,18 @@
|
||||
},
|
||||
"loginEnterPassword": "Enter the code",
|
||||
"loginSuccess": "Logged in as {}",
|
||||
"authFactorDelete": "Delete Auth Factor",
|
||||
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
|
||||
"authFactorPassword": "Password",
|
||||
"authFactorPasswordDescription": "The password you set when you registered.",
|
||||
"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!",
|
||||
"accountIntroSubtitle": "Pick an option below to get started.",
|
||||
"accountLogout": "Logout",
|
||||
@ -113,8 +127,14 @@
|
||||
"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",
|
||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||
"accountSettings": "Account Settings",
|
||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||
"accountProfileEdit": "Edit your profile",
|
||||
"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.",
|
||||
"publishersNew": "New Publisher",
|
||||
"publisherNewSubtitle": "Create a new publisher identity.",
|
||||
@ -128,6 +148,7 @@
|
||||
"one": "{} social point",
|
||||
"other": "{} social points"
|
||||
},
|
||||
"publisherAffiliatedBy": "Affiliated by {}",
|
||||
"publisherRunBy": "Run by {}",
|
||||
"fieldPublisherBelongToRealm": "Belongs to",
|
||||
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
||||
@ -138,6 +159,9 @@
|
||||
"fieldPostTitle": "Title",
|
||||
"fieldPostDescription": "Description",
|
||||
"fieldPostTags": "Tags",
|
||||
"fieldPostCategories": "Categories",
|
||||
"fieldPostAlias": "Alias",
|
||||
"fieldPostAliasHint": "Optional, used to represent the post in URL, should follow URL-Safe.",
|
||||
"postPublish": "Publish",
|
||||
"postPosted": "Post has been posted.",
|
||||
"postPublishedAt": "Published At",
|
||||
@ -175,12 +199,30 @@
|
||||
"other": "{} comments"
|
||||
},
|
||||
"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",
|
||||
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
||||
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
||||
"settingsBackgroundImageClearDescription": "Reset the background image to blank.",
|
||||
"settingsThemeMaterial3": "Use Material You 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",
|
||||
"settingsNetworkServer": "HyperNet Server",
|
||||
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||
@ -189,6 +231,13 @@
|
||||
"settingsNetworkServerPreset": "Present HyperNet Server",
|
||||
"settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.",
|
||||
"settingsNetworkServerSaved": "Server address saved.",
|
||||
"settingsPerformance": "Performance",
|
||||
"settingsImageQuality": "Image Quality",
|
||||
"settingsImageQualityDescription": "Set the image quality, it will affect the decoding speed of the image.",
|
||||
"settingsImageQualityLowest": "Lowest",
|
||||
"settingsImageQualityLow": "Low",
|
||||
"settingsImageQualityMedium": "Medium",
|
||||
"settingsImageQualityHigh": "High",
|
||||
"settingsMisc": "Misc",
|
||||
"settingsMiscAbout": "About",
|
||||
"settingsMiscAboutDescription": "View the version information of Solian.",
|
||||
@ -196,8 +245,9 @@
|
||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||
"sensitiveContentReveal": "Reveal",
|
||||
"serverConnecting": "Connecting to server...",
|
||||
"serverDisconnected": "Lost connection from server",
|
||||
"serverConnecting": "Connecting...",
|
||||
"serverDisconnected": "Connection Lost",
|
||||
"serverConnected": "Connected",
|
||||
"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.",
|
||||
"fieldChatName": "Name",
|
||||
@ -264,16 +314,50 @@
|
||||
"one": "{} attachment",
|
||||
"other": "{} attachments"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} is typing...",
|
||||
"other": "{} are typing..."
|
||||
},
|
||||
"fieldAttachmentRandomId": "Random ID",
|
||||
"fieldAttachmentAlt": "Alternative text",
|
||||
"addAttachmentFromAlbum": "Add from album",
|
||||
"addAttachmentFromClipboard": "Paste file",
|
||||
"addAttachmentFromCameraPhoto": "Take photo",
|
||||
"addAttachmentFromCameraVideo": "Take video",
|
||||
"addAttachmentFromRandomId": "Link via RID",
|
||||
"attachmentDetailInfo": "Attachment details",
|
||||
"attachmentPastedImage": "Pasted Image",
|
||||
"attachmentInsertLink": "Insert Link",
|
||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||
"attachmentCompressVideo": "Re-encode video",
|
||||
"attachmentSetThumbnail": "Set thumbnail",
|
||||
"attachmentSetAlt": "Set alternative text",
|
||||
"attachmentCopyRandomId": "Copy RID",
|
||||
"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",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "All notifications read",
|
||||
@ -361,7 +445,32 @@
|
||||
"dailyCheckNegativeHint5Description": "Lost connection at a crucial moment",
|
||||
"dailyCheckNegativeHint6": "Going out",
|
||||
"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",
|
||||
"friendRequests": "Friend Requests",
|
||||
"friendRequestsDescription": {
|
||||
@ -395,16 +504,20 @@
|
||||
"accountJoinedAt": "Joined at {}",
|
||||
"accountBirthday": "Born on {}",
|
||||
"accountBadge": "Badge",
|
||||
"badgeCompanyStaff": "Solsynth LLC Staff",
|
||||
"accountCheckInNoRecords": "No check-in records",
|
||||
"badgeCompanyStaff": "Solsynth Staff",
|
||||
"badgeSiteMigration": "Solar Network Native",
|
||||
"accountStatus": "Status",
|
||||
"accountStatusOnline": "Online",
|
||||
"accountStatusOffline": "Offline",
|
||||
"accountStatusLastSeen": "Last seen at {}",
|
||||
"postArticle": "Article on the Solar Network",
|
||||
"postStory": "Story on the Solar Network",
|
||||
"postLocalDraftRestored": "Restored from device",
|
||||
"articleWrittenAt": "Written at {}",
|
||||
"articleEditedAt": "Edited at {}",
|
||||
"attachmentSaved": "Saved to album",
|
||||
"attachmentSavedDesktop": "Saved to Downloads folder",
|
||||
"openInAlbum": "Open in album",
|
||||
"postAbuseReport": "Report Post",
|
||||
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
|
||||
@ -431,5 +544,61 @@
|
||||
"serviceStatus": "Service Status",
|
||||
"termRelated": "Related Terms",
|
||||
"appDetails": "App Details",
|
||||
"postRecommendation": "Highlight Posts"
|
||||
"postRecommendation": "Highlight Posts",
|
||||
"publisherBlockHint": "Block {}",
|
||||
"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.",
|
||||
"userBlocked": "{} has been blocked.",
|
||||
"postSharingViaPicture": "Capturing post as picture, please wait...",
|
||||
"postImageShareReadMore": "Scan the QR code to read full post",
|
||||
"postImageShareAds": "Explore posts on the Solar Network",
|
||||
"postShare": "Share",
|
||||
"postShareImage": "Share via Image",
|
||||
"appInitializing": "Initializing",
|
||||
"poweredBy": "Powered by {}",
|
||||
"shareIntent": "Share",
|
||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||
"shareIntentPostStory": "Post a Story",
|
||||
"updateAvailable": "Update Available",
|
||||
"updateOngoing": "Updating, please wait...",
|
||||
"custom": "Custom",
|
||||
"colorSchemeIndigo": "Indigo",
|
||||
"colorSchemeBlue": "Blue",
|
||||
"colorSchemeGreen": "Green",
|
||||
"colorSchemeYellow": "Yellow",
|
||||
"colorSchemeOrange": "Orange",
|
||||
"colorSchemeRed": "Red",
|
||||
"colorSchemeWhite": "White",
|
||||
"colorSchemeBlack": "Black",
|
||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||
"postCategoryTechnology": "Technology",
|
||||
"postCategoryGaming": "Gaming",
|
||||
"postCategoryLife": "Life",
|
||||
"postCategoryArts": "Arts",
|
||||
"postCategorySports": "Sports",
|
||||
"postCategoryMusic": "Music",
|
||||
"postCategoryNews": "News",
|
||||
"postCategoryKnowledge": "Knowledge",
|
||||
"postCategoryLiterature": "Literature",
|
||||
"postCategoryFunny": "Funny",
|
||||
"postCategoryUncategorized": "Uncategorized",
|
||||
"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"
|
||||
}
|
||||
}
|
@ -15,6 +15,10 @@
|
||||
"screenAccountProfileEdit": "编辑资料",
|
||||
"screenAbuseReport": "滥用检举",
|
||||
"screenSettings": "设置",
|
||||
"screenAccountSettings": "账号设置",
|
||||
"screenFactorSettings": "验证因子",
|
||||
"screenAccountWallet": "钱包",
|
||||
"screenNews": "新闻",
|
||||
"screenAlbum": "相册",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "编辑聊天频道",
|
||||
@ -87,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "验证代码",
|
||||
"loginSuccess": "登录为 {}",
|
||||
"authFactorDelete": "删除验证因子",
|
||||
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
|
||||
"authFactorPassword": "密码",
|
||||
"authFactorPasswordDescription": "注册时选择设置的密码。",
|
||||
"authFactorEmail": "电邮一次性验证码",
|
||||
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
|
||||
"authFactorTOTP": "时序验证码",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
|
||||
"authFactorInAppNotify": "应用内通知验证码",
|
||||
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
|
||||
"authFactorAdd": "添加新验证因子",
|
||||
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
|
||||
"accountIntroTitle": "喜欢您来!",
|
||||
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
||||
"accountLogout": "退出登录",
|
||||
@ -97,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
||||
"accountPublishers": "你的发布者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帐号设置",
|
||||
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
|
||||
"accountProfileEdit": "编辑资料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
||||
"accountWallet": "钱包",
|
||||
"accountWalletSubtitle": "查看你的余额和交易记录。",
|
||||
"factorSettings": "验证因子",
|
||||
"factorSettingsSubtitle": "管理你的登陆验证方式。",
|
||||
"accountProfileEditApplied": "个人资料修改已被应用。",
|
||||
"publishersNew": "新发布者",
|
||||
"publisherNewSubtitle": "创建一个新的公共身份。",
|
||||
@ -112,6 +132,7 @@
|
||||
"one": "{} 点社会信用点",
|
||||
"other": "{} 点社会信用点"
|
||||
},
|
||||
"publisherAffiliatedBy": "隶属于 {}",
|
||||
"publisherRunBy": "由 {} 管理",
|
||||
"fieldPublisherBelongToRealm": "所属领域",
|
||||
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
||||
@ -122,6 +143,9 @@
|
||||
"fieldPostTitle": "标题",
|
||||
"fieldPostDescription": "描述",
|
||||
"fieldPostTags": "标签",
|
||||
"fieldPostCategories": "分类",
|
||||
"fieldPostAlias": "别名",
|
||||
"fieldPostAliasHint": "可选项,用于在 URL 中表示该帖子,应遵循 URL-Safe 的原则。",
|
||||
"postPublish": "发布",
|
||||
"postPublishedAt": "发布于",
|
||||
"postPublishedUntil": "取消发布于",
|
||||
@ -173,12 +197,30 @@
|
||||
"other": "{} 条评论"
|
||||
},
|
||||
"settingsAppearance": "外观",
|
||||
"settingsDisplayLanguage": "显示语言",
|
||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||
"settingsDisplayLanguageSystem": "跟随系统",
|
||||
"settingsBackgroundImage": "背景图片",
|
||||
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||
"settingsBackgroundImageClear": "清除现存背景图",
|
||||
"settingsBackgroundImageClearDescription": "将应用背景图重置为空白。",
|
||||
"settingsThemeMaterial3": "使用 Material You 设计范式",
|
||||
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||
"settingsAppBarTransparent": "透明顶栏",
|
||||
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
|
||||
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
|
||||
"settingsColorScheme": "主题色",
|
||||
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||
"settingsColorSeed": "预设色彩主题",
|
||||
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知时振动",
|
||||
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
|
||||
"settingsExpandPostLink": "展开帖子链接",
|
||||
"settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
|
||||
"settingsExpandChatLink": "展开聊天链接",
|
||||
"settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
|
||||
"settingsNetwork": "网络",
|
||||
"settingsNetworkServer": "HyperNet 服务器",
|
||||
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||
@ -187,6 +229,13 @@
|
||||
"settingsNetworkServerPreset": "预设的 HyperNet 服务器",
|
||||
"settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。",
|
||||
"settingsNetworkServerSaved": "服务器地址已保存。",
|
||||
"settingsPerformance": "性能",
|
||||
"settingsImageQuality": "图片预览质量",
|
||||
"settingsImageQualityDescription": "设置图片预览质量,会影响图片解码速度。",
|
||||
"settingsImageQualityLowest": "极低",
|
||||
"settingsImageQualityLow": "低",
|
||||
"settingsImageQualityMedium": "中",
|
||||
"settingsImageQualityHigh": "高",
|
||||
"settingsMisc": "杂项",
|
||||
"settingsMiscAbout": "关于",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
@ -194,8 +243,9 @@
|
||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||
"sensitiveContentReveal": "显示内容",
|
||||
"serverConnecting": "正在连接服务器…",
|
||||
"serverDisconnected": "已与服务器断开连接",
|
||||
"serverConnecting": "正在连接…",
|
||||
"serverDisconnected": "已断开连接",
|
||||
"serverConnected": "已连接",
|
||||
"fieldChatAlias": "频道别名",
|
||||
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
|
||||
"fieldChatName": "名称",
|
||||
@ -262,16 +312,50 @@
|
||||
"one": "{} 个附件",
|
||||
"other": "{} 个附件"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} 正在输入",
|
||||
"other": "{} 正在输入"
|
||||
},
|
||||
"fieldAttachmentRandomId": "访问 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||
"addAttachmentFromClipboard": "粘贴附件",
|
||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||
"attachmentDetailInfo": "附件详细信息",
|
||||
"attachmentPastedImage": "粘贴的图片",
|
||||
"attachmentInsertLink": "插入连接",
|
||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||
"attachmentCompressVideo": "重新编码视频",
|
||||
"attachmentSetThumbnail": "设置缩略图",
|
||||
"attachmentSetAlt": "设置概述文字",
|
||||
"attachmentCopyRandomId": "复制访问 ID",
|
||||
"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": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "无未读通知",
|
||||
@ -359,7 +443,32 @@
|
||||
"dailyCheckNegativeHint5Description": "关键时刻断网",
|
||||
"dailyCheckNegativeHint6": "出门",
|
||||
"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": "添加好友",
|
||||
"friendRequests": "好友请求",
|
||||
"friendRequestsDescription": {
|
||||
@ -393,16 +502,20 @@
|
||||
"accountJoinedAt": "加入于 {}",
|
||||
"accountBirthday": "出生于 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暂无运势记录",
|
||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "状态",
|
||||
"accountStatusOnline": "在线",
|
||||
"accountStatusOffline": "离线",
|
||||
"accountStatusLastSeen": "最后一次在 {} 上线",
|
||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "从本地恢复草稿",
|
||||
"articleWrittenAt": "发表于 {}",
|
||||
"articleEditedAt": "编辑于 {}",
|
||||
"attachmentSaved": "已保存到相册",
|
||||
"attachmentSavedDesktop": "已保存到下载目录",
|
||||
"openInAlbum": "在相册中打开",
|
||||
"postAbuseReport": "检举帖子",
|
||||
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
|
||||
@ -429,5 +542,61 @@
|
||||
"serviceStatus": "服务状态",
|
||||
"termRelated": "相关条款",
|
||||
"appDetails": "应用程序详情",
|
||||
"postRecommendation": "推荐帖子"
|
||||
"postRecommendation": "推荐帖子",
|
||||
"publisherBlockHint": "屏蔽 {}",
|
||||
"publisherBlockHintDescription": "你正要屏蔽此发布者的运营者,该操作也将屏蔽由同一用户运营的发布者。",
|
||||
"userUnblocked": "已解除屏蔽用户 {}",
|
||||
"userBlocked": "已屏蔽用户 {}",
|
||||
"postSharingViaPicture": "正在生成帖子截图,请稍等片刻……",
|
||||
"postImageShareReadMore": "扫描右侧 QRCode 查看全文",
|
||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖图",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||
"shareIntentPostStory": "发布动态",
|
||||
"updateAvailable": "检测到更新可用",
|
||||
"updateOngoing": "正在更新,请稍后……",
|
||||
"custom": "自定义",
|
||||
"colorSchemeIndigo": "靛蓝",
|
||||
"colorSchemeBlue": "蓝色",
|
||||
"colorSchemeGreen": "绿色",
|
||||
"colorSchemeYellow": "黄色",
|
||||
"colorSchemeOrange": "橙色",
|
||||
"colorSchemeRed": "红色",
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||
"postCategoryTechnology": "技术",
|
||||
"postCategoryGaming": "游戏",
|
||||
"postCategoryLife": "生活",
|
||||
"postCategoryArts": "艺术",
|
||||
"postCategorySports": "体育",
|
||||
"postCategoryMusic": "音乐",
|
||||
"postCategoryNews": "新闻",
|
||||
"postCategoryKnowledge": "知识",
|
||||
"postCategoryLiterature": "文学",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分类",
|
||||
"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": "{} 源点"
|
||||
}
|
||||
}
|
602
assets/translations/zh-HK.json
Normal file
@ -0,0 +1,602 @@
|
||||
{
|
||||
"screen": "頁面",
|
||||
"screenAbout": "關於",
|
||||
"screenHome": "首頁",
|
||||
"screenExplore": "探索",
|
||||
"screenAccount": "您",
|
||||
"screenAuthLogin": "登陸",
|
||||
"screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
|
||||
"screenAuthLoginGreeting": "歡迎回來",
|
||||
"screenAuthRegister": "創建賬號",
|
||||
"screenAuthRegisterSubtitle": "創建一個 Solarpass 賬號",
|
||||
"screenAccountPublishers": "發佈者",
|
||||
"screenAccountPublisherNew": "新建發佈者",
|
||||
"screenAccountPublisherEdit": "編輯發佈者",
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "編輯聊天頻道",
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
"screenFriend": "好友",
|
||||
"dialogOkay": "好的",
|
||||
"dialogCancel": "取消",
|
||||
"dialogConfirm": "確認",
|
||||
"dialogDismiss": "忽略",
|
||||
"dialogError": "出了點問題",
|
||||
"errorRequestBad": "服務器拒絕了您的請求,請檢查您的輸入。",
|
||||
"errorRequestUnauthorized": "未授權的請求,請登錄或者嘗試重新登陸。",
|
||||
"errorRequestForbidden": "被禁止的請求,您沒有足夠的權限去做那件事。",
|
||||
"errorRequestNotFound": "您正查找的資源無法被找到。",
|
||||
"errorRequestConnection": "網絡連接錯誤,請檢查您的網絡狀態或者檢查我們的服務狀態。",
|
||||
"errorRequestUnknown": "未知請求錯誤,您可能想將此對話框截圖併發送給我們。",
|
||||
"unknown": "未知",
|
||||
"loading": "加載中…",
|
||||
"prev": "上一步",
|
||||
"next": "下一步",
|
||||
"edit": "編輯",
|
||||
"apply": "應用",
|
||||
"cancel": "取消",
|
||||
"create": "創建",
|
||||
"preview": "預覽",
|
||||
"delete": "刪除",
|
||||
"unlink": "解除鏈接",
|
||||
"crop": "裁剪",
|
||||
"compress": "壓縮",
|
||||
"report": "檢舉",
|
||||
"repost": "轉帖",
|
||||
"replyPost": "回貼",
|
||||
"reply": "回覆",
|
||||
"unset": "未設置",
|
||||
"untitled": "無題",
|
||||
"postDetail": "帖子詳情",
|
||||
"postNoun": "帖子",
|
||||
"postReadMore": "閲讀更多",
|
||||
"postReadEstimate": "預計花費 {} 閲讀",
|
||||
"postTotalLength": {
|
||||
"zero": "沒有內容",
|
||||
"one": "總計 {} 字",
|
||||
"other": "總計 {} 字"
|
||||
},
|
||||
"fieldUsername": "用户名",
|
||||
"fieldNickname": "顯示名",
|
||||
"fieldEmail": "電子郵箱地址",
|
||||
"fieldPassword": "密碼",
|
||||
"fieldUsernameAlphanumOnly": "用户名只能包含英文大小寫字母和數字。",
|
||||
"fieldUsernameLengthLimit": "用户名必須在 {} 和 {} 之間。",
|
||||
"fieldUsernameCannotEditHint": "用户名在創建後無法修改",
|
||||
"fieldUsernameLookupHint": "支持用户名、電話號碼或郵箱地址",
|
||||
"fieldNicknameLengthLimit": "暱稱必須在 {} 和 {} 之間。",
|
||||
"fieldEmailAddressMustBeValid": "電子郵箱地址必須是一個電子郵箱地址。",
|
||||
"fieldFirstName": "名",
|
||||
"fieldLastName": "姓",
|
||||
"fieldBirthday": "生日",
|
||||
"fieldImageHint": "你可以點擊這些個人頭像來編輯它們。",
|
||||
"fieldDescription": "簡介",
|
||||
"forgotPassword": "忘記密碼",
|
||||
"loginPickFactor": "選擇方式驗證",
|
||||
"loginMultiFactor": {
|
||||
"one": "{} 步驗證",
|
||||
"other": "{} 步驗證"
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
"accountLogoutSubtitle": "註銷當前賬户的登陸狀態。",
|
||||
"accountLogoutConfirmTitle": "您確定要退出登錄嗎?",
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
"publisherSyncWithAccount": "同步賬户信息",
|
||||
"publisherTotalUpvote": "總頂數",
|
||||
"publisherTotalDownvote": "總踩數",
|
||||
"publisherSocialPoint": "社會信用點",
|
||||
"publisherJoinedAt": "加入於 {}",
|
||||
"publisherSocialPointTotal": {
|
||||
"zero": "無社會信用點",
|
||||
"one": "{} 點社會信用點",
|
||||
"other": "{} 點社會信用點"
|
||||
},
|
||||
"publisherAffiliatedBy": "隸屬於 {}",
|
||||
"publisherRunBy": "由 {} 管理",
|
||||
"fieldPublisherBelongToRealm": "所屬領域",
|
||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
"fieldPostDescription": "描述",
|
||||
"fieldPostTags": "標籤",
|
||||
"fieldPostCategories": "分類",
|
||||
"fieldPostAlias": "別名",
|
||||
"fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
|
||||
"postPublish": "發佈",
|
||||
"postPublishedAt": "發佈於",
|
||||
"postPublishedUntil": "取消發佈於",
|
||||
"postVisibility": "可見性",
|
||||
"postVisibilityDescription": "帖子可見性決定了誰能查看該篇帖子。",
|
||||
"postVisibilityAll": "所有人可見",
|
||||
"postVisibilityFriends": "僅限好友可見",
|
||||
"postVisibilitySelected": "選定的用户可見",
|
||||
"postVisibilityFiltered": "選定用户不可見",
|
||||
"postVisibilityNone": "僅自己可見",
|
||||
"postVisibleUsers": "可見的用户",
|
||||
"postInvisibleUsers": "不可見的用户",
|
||||
"postSelectedUsers": {
|
||||
"zero": "未選擇用户",
|
||||
"one": "選擇了 {} 個用户",
|
||||
"other": "選擇了 {} 個用户"
|
||||
},
|
||||
"postEditingNotice": "你正在修改由 {} 發佈的帖子。",
|
||||
"postReplyingNotice": "你正在回覆由 {} 發佈的帖子。",
|
||||
"postRepostingNotice": "你正在轉發由 {} 發佈的帖子。",
|
||||
"postReact": "反應",
|
||||
"postPosted": "帖子已經發表。",
|
||||
"postReactions": "帖子的反應",
|
||||
"postReactionUpvote": {
|
||||
"zero": "0 個頂",
|
||||
"one": "{} 個頂",
|
||||
"other": "{} 個頂"
|
||||
},
|
||||
"postReactionDownvote": {
|
||||
"zero": "0 個踩",
|
||||
"one": "{} 個踩",
|
||||
"other": "{} 個踩"
|
||||
},
|
||||
"postReactionSocialPoint": {
|
||||
"zero": "無社會信用點變更",
|
||||
"one": "{} 點社會信用點變更",
|
||||
"other": "{} 點社會信用點變更"
|
||||
},
|
||||
"postReactCompleted": "反應已被添加。",
|
||||
"postReactUncompleted": "反應已被移除。",
|
||||
"postComments": {
|
||||
"zero": "評論",
|
||||
"one": "{} 條評論",
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"postCommentsDetailed": {
|
||||
"zero": "沒有評論",
|
||||
"one": "{} 條評論",
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
||||
"settingsThemeMaterial3": "使用 Material You 設計範式",
|
||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||
"settingsAppBarTransparent": "透明頂欄",
|
||||
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||
"settingsColorScheme": "主題色",
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"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": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
"fieldChatDescription": "描述",
|
||||
"fieldChatBelongToRealm": "所屬領域",
|
||||
"fieldChatBelongToRealmUnset": "未設置頻道所屬領域",
|
||||
"channelEditingNotice": "您正在編輯頻道 {}",
|
||||
"channelDeleted": "聊天頻道 {} 已被刪除",
|
||||
"channelDelete": "刪除聊天頻道 {}",
|
||||
"channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有消息將被永久刪除。",
|
||||
"channelDetailPersonalRegion": "個人區域",
|
||||
"channelDetailMemberRegion": "成員管理",
|
||||
"channelMemberManage": "管理成員",
|
||||
"channelMemberManageDescription": "管理頻道內現有成員。",
|
||||
"channelMemberAdd": "添加成員",
|
||||
"channelMemberAddDescription": "給當前頻道添加新成員。",
|
||||
"channelMemberAdded": "頻道成員已添加。",
|
||||
"fieldMemberRelatedName": "成員名 / 賬户 ID",
|
||||
"channelDetailAdminRegion": "管理區域",
|
||||
"channelEditProfile": "更改頻道身份",
|
||||
"channelEdit": "編輯頻道",
|
||||
"channelEditDescription": "更改頻道基本信息,元數據等。",
|
||||
"channelProfileEdit": "編輯頻道身份",
|
||||
"channelActionDelete": "刪除頻道",
|
||||
"channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有信息。",
|
||||
"channelLeave": "退出頻道 {}",
|
||||
"channelLeaveDescription": "退出該頻道,但是你頻道內的信息不會被移除。",
|
||||
"channelActionLeave": "退出頻道",
|
||||
"channelActionLeaveDescription": "刪除你在這個頻道的身份。",
|
||||
"channelNotifyLevel": "通知級別",
|
||||
"channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的消息。",
|
||||
"channelNotifyLevelAll": "全部通知",
|
||||
"channelNotifyLevelMentioned": "僅提及",
|
||||
"channelNotifyLevelNone": "全部靜音",
|
||||
"channelNotifyLevelApplied": "已經保存並應用頻道通知級別配置。",
|
||||
"fieldChannelProfileNick": "頻道內顯示名",
|
||||
"fieldChannelProfileNickHint": "在頻道內顯示的暱稱,留空則使用賬號顯示名。",
|
||||
"fieldRealmAlias": "領域別名",
|
||||
"fieldRealmAliasHint": "全站範圍內唯一的領域別名,用於在 URL 中表示該領域,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldRealmName": "名稱",
|
||||
"fieldRealmDescription": "描述",
|
||||
"realmEditingNotice": "您正在編輯領域 {}",
|
||||
"realmDeleted": "領域 {} 已被刪除",
|
||||
"realmDelete": "刪除領域 {}",
|
||||
"realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
|
||||
"realmActionDelete": "刪除領域",
|
||||
"realmActionDeleteDescription": "刪除整個領域及其附屬的資源。",
|
||||
"realmEdit": "編輯領域",
|
||||
"realmEditDescription": "更改領域基本信息,元數據等。",
|
||||
"realmMemberAdd": "添加成員",
|
||||
"realmMemberAddDescription": "給當前領域添加新成員。",
|
||||
"realmMemberAdded": "領域成員已添加。",
|
||||
"fieldChatMessage": "在 {} 中發消息",
|
||||
"fieldChatMessageDirect": "給 {} 發消息",
|
||||
"eventResourceTag": "消息 {}",
|
||||
"messageDelete": "刪除消息 {}",
|
||||
"messageDeleteDescription": "你確定要刪除這個消息嗎?該操作不可撤銷。同時您將留下一條刪除消息的記錄。",
|
||||
"messageDeleted": "消息 {} 已被刪除",
|
||||
"messageEdited": "消息 {} 已被編輯",
|
||||
"messageEditedHint": "已編輯",
|
||||
"messageUnsupported": "不支持的消息 {}",
|
||||
"messageFileHint": {
|
||||
"zero": "沒有附件",
|
||||
"one": "{} 個附件",
|
||||
"other": "{} 個附件"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} 正在輸入",
|
||||
"other": "{} 正在輸入"
|
||||
},
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
"attachmentCompressVideo": "重新編碼視頻",
|
||||
"attachmentSetThumbnail": "設置縮略圖",
|
||||
"attachmentSetAlt": "設置概述文字",
|
||||
"attachmentCopyRandomId": "複製訪問 ID",
|
||||
"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": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "無未讀通知",
|
||||
"one": "有 {} 個未讀通知",
|
||||
"other": "有 {} 個未讀通知"
|
||||
},
|
||||
"notificationUnread": "未讀",
|
||||
"notificationRead": "已讀",
|
||||
"notificationMarkAllRead": "已讀所有通知",
|
||||
"notificationMarkAllReadDescription": "您確定要將所有通知設置為已讀嗎?該操作不可撤銷。",
|
||||
"notificationMarkAllReadPrompt": {
|
||||
"zero": "已將 0 個通知標記為已讀。",
|
||||
"one": "已將 {} 個通知標記為已讀。",
|
||||
"other": "已將 {} 個通知標記為已讀。"
|
||||
},
|
||||
"notificationMarkOneReadPrompt": "已將通知 {} 標記為已讀。",
|
||||
"search": "搜索",
|
||||
"postSearchResult": {
|
||||
"zero": "沒有搜索到結果",
|
||||
"one": "搜索到 {} 個結果",
|
||||
"other": "搜索到 {} 個結果"
|
||||
},
|
||||
"postSearchTook": "耗時 {}",
|
||||
"postDelete": "刪除帖子 {}",
|
||||
"postDeleteDescription": "你確定要刪除這個帖子嗎?該操作不可撤銷。",
|
||||
"postDeleted": "帖子 {} 已被刪除。",
|
||||
"call": "通話",
|
||||
"callOngoingNotice": "一則通話進行中",
|
||||
"callJoin": "加入",
|
||||
"callResume": "恢復",
|
||||
"callMicrophone": "麥克風",
|
||||
"callCamera": "攝像頭",
|
||||
"callMicrophoneDisabled": "麥克風已禁用",
|
||||
"callMicrophoneSelect": "選擇麥克風",
|
||||
"callCameraDisabled": "攝像頭已禁用",
|
||||
"callCameraSelect": "選擇攝像頭",
|
||||
"callDisconnected": "通話已斷開",
|
||||
"callEnded": "通話已結束",
|
||||
"callStatusConnected": "已連接",
|
||||
"callStatusDisconnected": "未連接",
|
||||
"callStatusConnecting": "正在連接",
|
||||
"callStatusReconnecting": "正在重連",
|
||||
"callDisconnect": "斷開連接",
|
||||
"callDisconnectDescription": "您確定要與通話斷開連接嗎?",
|
||||
"callMicrophoneOff": "關閉麥克風",
|
||||
"callMicrophoneOn": "打開麥克風",
|
||||
"callCameraOff": "關閉攝像頭",
|
||||
"callCameraOn": "打開攝像頭",
|
||||
"callVideoFlip": "鏡像畫面",
|
||||
"callSpeakerphoneToggle": "切換揚聲器",
|
||||
"callScreenOff": "關閉屏幕共享",
|
||||
"callScreenOn": "開啓屏幕共享",
|
||||
"callMessageEnded": "通話持續了 {}",
|
||||
"callMessageStarted": "通話開始了",
|
||||
"dailyCheckIn": "每日簽到",
|
||||
"dailyCheckInNone": "今日尚未簽到",
|
||||
"dailyCheckAction": "現在簽到",
|
||||
"dailyCheckDetail": "看不懂符?大師幫我解惑!",
|
||||
"dailyCheckDetailTitle": "{} 的運勢詳情",
|
||||
"dailyCheckPositiveHint": "宜 {}",
|
||||
"dailyCheckNegativeHint": "忌 {}",
|
||||
"dailyCheckEverythingIsPositive": "諸事皆宜",
|
||||
"dailyCheckEverythingIsNegative": "諸事不宜",
|
||||
"dailyCheckPositiveHint1": "交友",
|
||||
"dailyCheckPositiveHint1Description": "友誼地久天長",
|
||||
"dailyCheckPositiveHint2": "飲酒",
|
||||
"dailyCheckPositiveHint2Description": "對影成三人",
|
||||
"dailyCheckPositiveHint3": "旅行",
|
||||
"dailyCheckPositiveHint3Description": "千里之行,始於足下",
|
||||
"dailyCheckPositiveHint4": "運動",
|
||||
"dailyCheckPositiveHint4Description": "生命在於運動",
|
||||
"dailyCheckPositiveHint5": "學習",
|
||||
"dailyCheckPositiveHint5Description": "學無止境,日有所進",
|
||||
"dailyCheckPositiveHint6": "種植",
|
||||
"dailyCheckPositiveHint6Description": "種下希望,收穫未來",
|
||||
"dailyCheckNegativeHint1": "吃飯",
|
||||
"dailyCheckNegativeHint1Description": "吃飯咬到舌頭",
|
||||
"dailyCheckNegativeHint2": "考試",
|
||||
"dailyCheckNegativeHint2Description": "考的東西剛好沒複習",
|
||||
"dailyCheckNegativeHint3": "坐公交",
|
||||
"dailyCheckNegativeHint3Description": "趕車剛好錯過一班",
|
||||
"dailyCheckNegativeHint4": "購物",
|
||||
"dailyCheckNegativeHint4Description": "買回來的衣服發現不合適",
|
||||
"dailyCheckNegativeHint5": "打遊戲",
|
||||
"dailyCheckNegativeHint5Description": "關鍵時刻斷網",
|
||||
"dailyCheckNegativeHint6": "出門",
|
||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||
"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": "添加好友",
|
||||
"friendRequests": "好友請求",
|
||||
"friendRequestsDescription": {
|
||||
"zero": "你沒有好友請求",
|
||||
"one": "你有 {} 個好友請求",
|
||||
"other": "你有 {} 個好友請求"
|
||||
},
|
||||
"friendBlocklist": "屏蔽列表",
|
||||
"friendBlocklistDescription": {
|
||||
"zero": "你沒有屏蔽任何人",
|
||||
"one": "你屏蔽了 {} 個用户",
|
||||
"other": "你屏蔽了 {} 個用户"
|
||||
},
|
||||
"friendStatusPending": "待處理",
|
||||
"friendStatusWaiting": "等待中",
|
||||
"friendStatusActive": "正活躍",
|
||||
"friendStatusBlocked": "已屏蔽",
|
||||
"friendRequestSent": "好友請求已發送。",
|
||||
"fieldFriendRelatedName": "好友名 / 賬户 ID",
|
||||
"friendBlock": "屏蔽",
|
||||
"friendUnblock": "解除屏蔽",
|
||||
"friendDeleteAction": "遺忘",
|
||||
"friendDelete": "遺忘跟 {} 的關係",
|
||||
"friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
|
||||
"friendRequestAccept": "接受",
|
||||
"friendRequestDecline": "拒絕",
|
||||
"subscribe": "訂閲",
|
||||
"unsubscribe": "取消訂閲",
|
||||
"attachmentUploadBy": "上傳者",
|
||||
"attachmentShotOn": "由 {} 拍攝",
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
"accountStatusOnline": "在線",
|
||||
"accountStatusOffline": "離線",
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
"attachmentSavedDesktop": "已保存到下載目錄",
|
||||
"openInAlbum": "在相冊中打開",
|
||||
"postAbuseReport": "檢舉帖子",
|
||||
"postAbuseReportDescription": "檢舉不符合我們用户協議以及社區準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
|
||||
"abuseReport": "檢舉",
|
||||
"abuseReportDescription": "檢舉不符合我們用户協議以及社區準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
|
||||
"abuseReportAction": "提交檢舉",
|
||||
"abuseReportActionDescription": "檢舉不合規行為。",
|
||||
"abuseReportResource": "資源位置 / ID",
|
||||
"abuseReportReason": "檢舉原因",
|
||||
"abuseReportSubmitted": "檢舉已提交,感謝你的貢獻。",
|
||||
"submit": "提交",
|
||||
"accountDeletion": "刪除帳户",
|
||||
"accountDeletionDescription": "你確定要刪除這個帳户嗎?該操作不可撤銷,其隸屬於該帳户的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
|
||||
"accountDeletionActionDescription": "刪除你的 Solarpass 帳户。",
|
||||
"accountDeletionSubmitted": "帳户刪除申請已發出,你可以檢查你的收件箱並根據郵件內的指示完成刪除操作。",
|
||||
"channelNewChannel": "新建頻道",
|
||||
"channelNewDirectMessage": "發起私信",
|
||||
"channelDirectMessageDescription": "與 {} 的私聊",
|
||||
"fieldCannotBeEmpty": "此字段不能為空。",
|
||||
"termAcceptLink": "瀏覽條款",
|
||||
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
||||
"unauthorized": "未登陸",
|
||||
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
||||
"serviceStatus": "服務狀態",
|
||||
"termRelated": "相關條款",
|
||||
"appDetails": "應用程序詳情",
|
||||
"postRecommendation": "推薦帖子",
|
||||
"publisherBlockHint": "屏蔽 {}",
|
||||
"publisherBlockHintDescription": "你正要屏蔽此發佈者的運營者,該操作也將屏蔽由同一用户運營的發佈者。",
|
||||
"userUnblocked": "已解除屏蔽用户 {}",
|
||||
"userBlocked": "已屏蔽用户 {}",
|
||||
"postSharingViaPicture": "正在生成帖子截圖,請稍等片刻……",
|
||||
"postImageShareReadMore": "掃描右側 QRCode 查看全文",
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
"colorSchemeIndigo": "靛藍",
|
||||
"colorSchemeBlue": "藍色",
|
||||
"colorSchemeGreen": "綠色",
|
||||
"colorSchemeYellow": "黃色",
|
||||
"colorSchemeOrange": "橙色",
|
||||
"colorSchemeRed": "紅色",
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
"postCategoryArts": "藝術",
|
||||
"postCategorySports": "體育",
|
||||
"postCategoryMusic": "音樂",
|
||||
"postCategoryNews": "新聞",
|
||||
"postCategoryKnowledge": "知識",
|
||||
"postCategoryLiterature": "文學",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分類",
|
||||
"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": "{} 源點"
|
||||
}
|
||||
}
|
602
assets/translations/zh-TW.json
Normal file
@ -0,0 +1,602 @@
|
||||
{
|
||||
"screen": "頁面",
|
||||
"screenAbout": "關於",
|
||||
"screenHome": "首頁",
|
||||
"screenExplore": "探索",
|
||||
"screenAccount": "您",
|
||||
"screenAuthLogin": "登陸",
|
||||
"screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
|
||||
"screenAuthLoginGreeting": "歡迎回來",
|
||||
"screenAuthRegister": "創建賬號",
|
||||
"screenAuthRegisterSubtitle": "創建一個 Solarpass 賬號",
|
||||
"screenAccountPublishers": "發佈者",
|
||||
"screenAccountPublisherNew": "新建發佈者",
|
||||
"screenAccountPublisherEdit": "編輯發佈者",
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "編輯聊天頻道",
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
"screenFriend": "好友",
|
||||
"dialogOkay": "好的",
|
||||
"dialogCancel": "取消",
|
||||
"dialogConfirm": "確認",
|
||||
"dialogDismiss": "忽略",
|
||||
"dialogError": "出了點問題",
|
||||
"errorRequestBad": "服務器拒絕了您的請求,請檢查您的輸入。",
|
||||
"errorRequestUnauthorized": "未授權的請求,請登錄或者嘗試重新登陸。",
|
||||
"errorRequestForbidden": "被禁止的請求,您沒有足夠的權限去做那件事。",
|
||||
"errorRequestNotFound": "您正查找的資源無法被找到。",
|
||||
"errorRequestConnection": "網絡連接錯誤,請檢查您的網絡狀態或者檢查我們的服務狀態。",
|
||||
"errorRequestUnknown": "未知請求錯誤,您可能想將此對話框截圖併發送給我們。",
|
||||
"unknown": "未知",
|
||||
"loading": "加載中…",
|
||||
"prev": "上一步",
|
||||
"next": "下一步",
|
||||
"edit": "編輯",
|
||||
"apply": "應用",
|
||||
"cancel": "取消",
|
||||
"create": "創建",
|
||||
"preview": "預覽",
|
||||
"delete": "刪除",
|
||||
"unlink": "解除鏈接",
|
||||
"crop": "裁剪",
|
||||
"compress": "壓縮",
|
||||
"report": "檢舉",
|
||||
"repost": "轉帖",
|
||||
"replyPost": "回貼",
|
||||
"reply": "回覆",
|
||||
"unset": "未設置",
|
||||
"untitled": "無題",
|
||||
"postDetail": "帖子詳情",
|
||||
"postNoun": "帖子",
|
||||
"postReadMore": "閱讀更多",
|
||||
"postReadEstimate": "預計花費 {} 閱讀",
|
||||
"postTotalLength": {
|
||||
"zero": "沒有內容",
|
||||
"one": "總計 {} 字",
|
||||
"other": "總計 {} 字"
|
||||
},
|
||||
"fieldUsername": "用戶名",
|
||||
"fieldNickname": "顯示名",
|
||||
"fieldEmail": "電子郵箱地址",
|
||||
"fieldPassword": "密碼",
|
||||
"fieldUsernameAlphanumOnly": "用戶名只能包含英文大小寫字母和數字。",
|
||||
"fieldUsernameLengthLimit": "用戶名必須在 {} 和 {} 之間。",
|
||||
"fieldUsernameCannotEditHint": "用戶名在創建後無法修改",
|
||||
"fieldUsernameLookupHint": "支持用戶名、電話號碼或郵箱地址",
|
||||
"fieldNicknameLengthLimit": "暱稱必須在 {} 和 {} 之間。",
|
||||
"fieldEmailAddressMustBeValid": "電子郵箱地址必須是一個電子郵箱地址。",
|
||||
"fieldFirstName": "名",
|
||||
"fieldLastName": "姓",
|
||||
"fieldBirthday": "生日",
|
||||
"fieldImageHint": "你可以點擊這些個人頭像來編輯它們。",
|
||||
"fieldDescription": "簡介",
|
||||
"forgotPassword": "忘記密碼",
|
||||
"loginPickFactor": "選擇方式驗證",
|
||||
"loginMultiFactor": {
|
||||
"one": "{} 步驗證",
|
||||
"other": "{} 步驗證"
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
"accountLogoutSubtitle": "註銷當前賬戶的登陸狀態。",
|
||||
"accountLogoutConfirmTitle": "您確定要退出登錄嗎?",
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
"publisherSyncWithAccount": "同步賬戶信息",
|
||||
"publisherTotalUpvote": "總頂數",
|
||||
"publisherTotalDownvote": "總踩數",
|
||||
"publisherSocialPoint": "社會信用點",
|
||||
"publisherJoinedAt": "加入於 {}",
|
||||
"publisherSocialPointTotal": {
|
||||
"zero": "無社會信用點",
|
||||
"one": "{} 點社會信用點",
|
||||
"other": "{} 點社會信用點"
|
||||
},
|
||||
"publisherAffiliatedBy": "隸屬於 {}",
|
||||
"publisherRunBy": "由 {} 管理",
|
||||
"fieldPublisherBelongToRealm": "所屬領域",
|
||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
"fieldPostDescription": "描述",
|
||||
"fieldPostTags": "標籤",
|
||||
"fieldPostCategories": "分類",
|
||||
"fieldPostAlias": "別名",
|
||||
"fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
|
||||
"postPublish": "發佈",
|
||||
"postPublishedAt": "發佈於",
|
||||
"postPublishedUntil": "取消發佈於",
|
||||
"postVisibility": "可見性",
|
||||
"postVisibilityDescription": "帖子可見性決定了誰能查看該篇帖子。",
|
||||
"postVisibilityAll": "所有人可見",
|
||||
"postVisibilityFriends": "僅限好友可見",
|
||||
"postVisibilitySelected": "選定的用戶可見",
|
||||
"postVisibilityFiltered": "選定用戶不可見",
|
||||
"postVisibilityNone": "僅自己可見",
|
||||
"postVisibleUsers": "可見的用戶",
|
||||
"postInvisibleUsers": "不可見的用戶",
|
||||
"postSelectedUsers": {
|
||||
"zero": "未選擇用戶",
|
||||
"one": "選擇了 {} 個用戶",
|
||||
"other": "選擇了 {} 個用戶"
|
||||
},
|
||||
"postEditingNotice": "你正在修改由 {} 發佈的帖子。",
|
||||
"postReplyingNotice": "你正在回覆由 {} 發佈的帖子。",
|
||||
"postRepostingNotice": "你正在轉發由 {} 發佈的帖子。",
|
||||
"postReact": "反應",
|
||||
"postPosted": "帖子已經發表。",
|
||||
"postReactions": "帖子的反應",
|
||||
"postReactionUpvote": {
|
||||
"zero": "0 個頂",
|
||||
"one": "{} 個頂",
|
||||
"other": "{} 個頂"
|
||||
},
|
||||
"postReactionDownvote": {
|
||||
"zero": "0 個踩",
|
||||
"one": "{} 個踩",
|
||||
"other": "{} 個踩"
|
||||
},
|
||||
"postReactionSocialPoint": {
|
||||
"zero": "無社會信用點變更",
|
||||
"one": "{} 點社會信用點變更",
|
||||
"other": "{} 點社會信用點變更"
|
||||
},
|
||||
"postReactCompleted": "反應已被添加。",
|
||||
"postReactUncompleted": "反應已被移除。",
|
||||
"postComments": {
|
||||
"zero": "評論",
|
||||
"one": "{} 條評論",
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"postCommentsDetailed": {
|
||||
"zero": "沒有評論",
|
||||
"one": "{} 條評論",
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
||||
"settingsThemeMaterial3": "使用 Material You 設計範式",
|
||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||
"settingsAppBarTransparent": "透明頂欄",
|
||||
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||
"settingsColorScheme": "主題色",
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"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": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
"fieldChatDescription": "描述",
|
||||
"fieldChatBelongToRealm": "所屬領域",
|
||||
"fieldChatBelongToRealmUnset": "未設置頻道所屬領域",
|
||||
"channelEditingNotice": "您正在編輯頻道 {}",
|
||||
"channelDeleted": "聊天頻道 {} 已被刪除",
|
||||
"channelDelete": "刪除聊天頻道 {}",
|
||||
"channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有消息將被永久刪除。",
|
||||
"channelDetailPersonalRegion": "個人區域",
|
||||
"channelDetailMemberRegion": "成員管理",
|
||||
"channelMemberManage": "管理成員",
|
||||
"channelMemberManageDescription": "管理頻道內現有成員。",
|
||||
"channelMemberAdd": "添加成員",
|
||||
"channelMemberAddDescription": "給當前頻道添加新成員。",
|
||||
"channelMemberAdded": "頻道成員已添加。",
|
||||
"fieldMemberRelatedName": "成員名 / 賬戶 ID",
|
||||
"channelDetailAdminRegion": "管理區域",
|
||||
"channelEditProfile": "更改頻道身份",
|
||||
"channelEdit": "編輯頻道",
|
||||
"channelEditDescription": "更改頻道基本信息,元數據等。",
|
||||
"channelProfileEdit": "編輯頻道身份",
|
||||
"channelActionDelete": "刪除頻道",
|
||||
"channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有信息。",
|
||||
"channelLeave": "退出頻道 {}",
|
||||
"channelLeaveDescription": "退出該頻道,但是你頻道內的信息不會被移除。",
|
||||
"channelActionLeave": "退出頻道",
|
||||
"channelActionLeaveDescription": "刪除你在這個頻道的身份。",
|
||||
"channelNotifyLevel": "通知級別",
|
||||
"channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的消息。",
|
||||
"channelNotifyLevelAll": "全部通知",
|
||||
"channelNotifyLevelMentioned": "僅提及",
|
||||
"channelNotifyLevelNone": "全部靜音",
|
||||
"channelNotifyLevelApplied": "已經保存並應用頻道通知級別配置。",
|
||||
"fieldChannelProfileNick": "頻道內顯示名",
|
||||
"fieldChannelProfileNickHint": "在頻道內顯示的暱稱,留空則使用賬號顯示名。",
|
||||
"fieldRealmAlias": "領域別名",
|
||||
"fieldRealmAliasHint": "全站範圍內唯一的領域別名,用於在 URL 中表示該領域,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldRealmName": "名稱",
|
||||
"fieldRealmDescription": "描述",
|
||||
"realmEditingNotice": "您正在編輯領域 {}",
|
||||
"realmDeleted": "領域 {} 已被刪除",
|
||||
"realmDelete": "刪除領域 {}",
|
||||
"realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
|
||||
"realmActionDelete": "刪除領域",
|
||||
"realmActionDeleteDescription": "刪除整個領域及其附屬的資源。",
|
||||
"realmEdit": "編輯領域",
|
||||
"realmEditDescription": "更改領域基本信息,元數據等。",
|
||||
"realmMemberAdd": "添加成員",
|
||||
"realmMemberAddDescription": "給當前領域添加新成員。",
|
||||
"realmMemberAdded": "領域成員已添加。",
|
||||
"fieldChatMessage": "在 {} 中發消息",
|
||||
"fieldChatMessageDirect": "給 {} 發消息",
|
||||
"eventResourceTag": "消息 {}",
|
||||
"messageDelete": "刪除消息 {}",
|
||||
"messageDeleteDescription": "你確定要刪除這個消息嗎?該操作不可撤銷。同時您將留下一條刪除消息的記錄。",
|
||||
"messageDeleted": "消息 {} 已被刪除",
|
||||
"messageEdited": "消息 {} 已被編輯",
|
||||
"messageEditedHint": "已編輯",
|
||||
"messageUnsupported": "不支持的消息 {}",
|
||||
"messageFileHint": {
|
||||
"zero": "沒有附件",
|
||||
"one": "{} 個附件",
|
||||
"other": "{} 個附件"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} 正在輸入",
|
||||
"other": "{} 正在輸入"
|
||||
},
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
"attachmentCompressVideo": "重新編碼視頻",
|
||||
"attachmentSetThumbnail": "設置縮略圖",
|
||||
"attachmentSetAlt": "設置概述文字",
|
||||
"attachmentCopyRandomId": "複製訪問 ID",
|
||||
"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": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "無未讀通知",
|
||||
"one": "有 {} 個未讀通知",
|
||||
"other": "有 {} 個未讀通知"
|
||||
},
|
||||
"notificationUnread": "未讀",
|
||||
"notificationRead": "已讀",
|
||||
"notificationMarkAllRead": "已讀所有通知",
|
||||
"notificationMarkAllReadDescription": "您確定要將所有通知設置為已讀嗎?該操作不可撤銷。",
|
||||
"notificationMarkAllReadPrompt": {
|
||||
"zero": "已將 0 個通知標記為已讀。",
|
||||
"one": "已將 {} 個通知標記為已讀。",
|
||||
"other": "已將 {} 個通知標記為已讀。"
|
||||
},
|
||||
"notificationMarkOneReadPrompt": "已將通知 {} 標記為已讀。",
|
||||
"search": "搜索",
|
||||
"postSearchResult": {
|
||||
"zero": "沒有搜索到結果",
|
||||
"one": "搜索到 {} 個結果",
|
||||
"other": "搜索到 {} 個結果"
|
||||
},
|
||||
"postSearchTook": "耗時 {}",
|
||||
"postDelete": "刪除帖子 {}",
|
||||
"postDeleteDescription": "你確定要刪除這個帖子嗎?該操作不可撤銷。",
|
||||
"postDeleted": "帖子 {} 已被刪除。",
|
||||
"call": "通話",
|
||||
"callOngoingNotice": "一則通話進行中",
|
||||
"callJoin": "加入",
|
||||
"callResume": "恢復",
|
||||
"callMicrophone": "麥克風",
|
||||
"callCamera": "攝像頭",
|
||||
"callMicrophoneDisabled": "麥克風已禁用",
|
||||
"callMicrophoneSelect": "選擇麥克風",
|
||||
"callCameraDisabled": "攝像頭已禁用",
|
||||
"callCameraSelect": "選擇攝像頭",
|
||||
"callDisconnected": "通話已斷開",
|
||||
"callEnded": "通話已結束",
|
||||
"callStatusConnected": "已連接",
|
||||
"callStatusDisconnected": "未連接",
|
||||
"callStatusConnecting": "正在連接",
|
||||
"callStatusReconnecting": "正在重連",
|
||||
"callDisconnect": "斷開連接",
|
||||
"callDisconnectDescription": "您確定要與通話斷開連接嗎?",
|
||||
"callMicrophoneOff": "關閉麥克風",
|
||||
"callMicrophoneOn": "打開麥克風",
|
||||
"callCameraOff": "關閉攝像頭",
|
||||
"callCameraOn": "打開攝像頭",
|
||||
"callVideoFlip": "鏡像畫面",
|
||||
"callSpeakerphoneToggle": "切換揚聲器",
|
||||
"callScreenOff": "關閉屏幕共享",
|
||||
"callScreenOn": "開啟屏幕共享",
|
||||
"callMessageEnded": "通話持續了 {}",
|
||||
"callMessageStarted": "通話開始了",
|
||||
"dailyCheckIn": "每日簽到",
|
||||
"dailyCheckInNone": "今日尚未簽到",
|
||||
"dailyCheckAction": "現在簽到",
|
||||
"dailyCheckDetail": "看不懂符?大師幫我解惑!",
|
||||
"dailyCheckDetailTitle": "{} 的運勢詳情",
|
||||
"dailyCheckPositiveHint": "宜 {}",
|
||||
"dailyCheckNegativeHint": "忌 {}",
|
||||
"dailyCheckEverythingIsPositive": "諸事皆宜",
|
||||
"dailyCheckEverythingIsNegative": "諸事不宜",
|
||||
"dailyCheckPositiveHint1": "交友",
|
||||
"dailyCheckPositiveHint1Description": "友誼地久天長",
|
||||
"dailyCheckPositiveHint2": "飲酒",
|
||||
"dailyCheckPositiveHint2Description": "對影成三人",
|
||||
"dailyCheckPositiveHint3": "旅行",
|
||||
"dailyCheckPositiveHint3Description": "千里之行,始於足下",
|
||||
"dailyCheckPositiveHint4": "運動",
|
||||
"dailyCheckPositiveHint4Description": "生命在於運動",
|
||||
"dailyCheckPositiveHint5": "學習",
|
||||
"dailyCheckPositiveHint5Description": "學無止境,日有所進",
|
||||
"dailyCheckPositiveHint6": "種植",
|
||||
"dailyCheckPositiveHint6Description": "種下希望,收穫未來",
|
||||
"dailyCheckNegativeHint1": "吃飯",
|
||||
"dailyCheckNegativeHint1Description": "吃飯咬到舌頭",
|
||||
"dailyCheckNegativeHint2": "考試",
|
||||
"dailyCheckNegativeHint2Description": "考的東西剛好沒複習",
|
||||
"dailyCheckNegativeHint3": "坐公交",
|
||||
"dailyCheckNegativeHint3Description": "趕車剛好錯過一班",
|
||||
"dailyCheckNegativeHint4": "購物",
|
||||
"dailyCheckNegativeHint4Description": "買回來的衣服發現不合適",
|
||||
"dailyCheckNegativeHint5": "打遊戲",
|
||||
"dailyCheckNegativeHint5Description": "關鍵時刻斷網",
|
||||
"dailyCheckNegativeHint6": "出門",
|
||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||
"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": "添加好友",
|
||||
"friendRequests": "好友請求",
|
||||
"friendRequestsDescription": {
|
||||
"zero": "你沒有好友請求",
|
||||
"one": "你有 {} 個好友請求",
|
||||
"other": "你有 {} 個好友請求"
|
||||
},
|
||||
"friendBlocklist": "屏蔽列表",
|
||||
"friendBlocklistDescription": {
|
||||
"zero": "你沒有屏蔽任何人",
|
||||
"one": "你屏蔽了 {} 個用戶",
|
||||
"other": "你屏蔽了 {} 個用戶"
|
||||
},
|
||||
"friendStatusPending": "待處理",
|
||||
"friendStatusWaiting": "等待中",
|
||||
"friendStatusActive": "正活躍",
|
||||
"friendStatusBlocked": "已屏蔽",
|
||||
"friendRequestSent": "好友請求已發送。",
|
||||
"fieldFriendRelatedName": "好友名 / 賬戶 ID",
|
||||
"friendBlock": "屏蔽",
|
||||
"friendUnblock": "解除屏蔽",
|
||||
"friendDeleteAction": "遺忘",
|
||||
"friendDelete": "遺忘跟 {} 的關係",
|
||||
"friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
|
||||
"friendRequestAccept": "接受",
|
||||
"friendRequestDecline": "拒絕",
|
||||
"subscribe": "訂閱",
|
||||
"unsubscribe": "取消訂閱",
|
||||
"attachmentUploadBy": "上傳者",
|
||||
"attachmentShotOn": "由 {} 拍攝",
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
"accountStatusOnline": "在線",
|
||||
"accountStatusOffline": "離線",
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
"attachmentSavedDesktop": "已保存到下載目錄",
|
||||
"openInAlbum": "在相冊中打開",
|
||||
"postAbuseReport": "檢舉帖子",
|
||||
"postAbuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
|
||||
"abuseReport": "檢舉",
|
||||
"abuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
|
||||
"abuseReportAction": "提交檢舉",
|
||||
"abuseReportActionDescription": "檢舉不合規行為。",
|
||||
"abuseReportResource": "資源位置 / ID",
|
||||
"abuseReportReason": "檢舉原因",
|
||||
"abuseReportSubmitted": "檢舉已提交,感謝你的貢獻。",
|
||||
"submit": "提交",
|
||||
"accountDeletion": "刪除帳戶",
|
||||
"accountDeletionDescription": "你確定要刪除這個帳戶嗎?該操作不可撤銷,其隸屬於該帳戶的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
|
||||
"accountDeletionActionDescription": "刪除你的 Solarpass 帳戶。",
|
||||
"accountDeletionSubmitted": "帳戶刪除申請已發出,你可以檢查你的收件箱並根據郵件內的指示完成刪除操作。",
|
||||
"channelNewChannel": "新建頻道",
|
||||
"channelNewDirectMessage": "發起私信",
|
||||
"channelDirectMessageDescription": "與 {} 的私聊",
|
||||
"fieldCannotBeEmpty": "此字段不能為空。",
|
||||
"termAcceptLink": "瀏覽條款",
|
||||
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
||||
"unauthorized": "未登陸",
|
||||
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
||||
"serviceStatus": "服務狀態",
|
||||
"termRelated": "相關條款",
|
||||
"appDetails": "應用程序詳情",
|
||||
"postRecommendation": "推薦帖子",
|
||||
"publisherBlockHint": "屏蔽 {}",
|
||||
"publisherBlockHintDescription": "你正要屏蔽此發佈者的運營者,該操作也將屏蔽由同一用戶運營的發佈者。",
|
||||
"userUnblocked": "已解除屏蔽用戶 {}",
|
||||
"userBlocked": "已屏蔽用戶 {}",
|
||||
"postSharingViaPicture": "正在生成帖子截圖,請稍等片刻……",
|
||||
"postImageShareReadMore": "掃描右側 QRCode 查看全文",
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
"colorSchemeIndigo": "靛藍",
|
||||
"colorSchemeBlue": "藍色",
|
||||
"colorSchemeGreen": "綠色",
|
||||
"colorSchemeYellow": "黃色",
|
||||
"colorSchemeOrange": "橙色",
|
||||
"colorSchemeRed": "紅色",
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
"postCategoryArts": "藝術",
|
||||
"postCategorySports": "體育",
|
||||
"postCategoryMusic": "音樂",
|
||||
"postCategoryNews": "新聞",
|
||||
"postCategoryKnowledge": "知識",
|
||||
"postCategoryLiterature": "文學",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分類",
|
||||
"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": "{} 源點"
|
||||
}
|
||||
}
|
22
ios/Podfile
@ -35,6 +35,28 @@ target 'Runner' do
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
|
||||
target 'SolarNotifyService' do
|
||||
inherit! :search_paths
|
||||
pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios'
|
||||
|
||||
pod 'Kingfisher', '~> 8.0'
|
||||
pod 'Alamofire'
|
||||
end
|
||||
|
||||
target 'SolarWidgetExtension' do
|
||||
inherit! :search_paths
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios'
|
||||
|
||||
pod 'Kingfisher', '~> 8.0'
|
||||
end
|
||||
|
||||
target 'SolarShare' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
184
ios/Podfile.lock
@ -1,11 +1,10 @@
|
||||
PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- Flutter
|
||||
- cupertino_http (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
@ -42,58 +41,60 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.4.0):
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.6.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.4.0):
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.4.0)
|
||||
- Firebase/CoreOnly (11.4.0):
|
||||
- FirebaseCore (= 11.4.0)
|
||||
- Firebase/Messaging (11.4.0):
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.4.0)
|
||||
- firebase_analytics (11.3.6):
|
||||
- Firebase/Analytics (= 11.4.0)
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.1):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.8.1):
|
||||
- Firebase/CoreOnly (= 11.4.0)
|
||||
- firebase_core (3.10.1):
|
||||
- Firebase/CoreOnly (= 11.6.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.1.6):
|
||||
- Firebase/Messaging (= 11.4.0)
|
||||
- firebase_messaging (15.2.1):
|
||||
- Firebase/Messaging (= 11.6.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (11.4.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.4.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.4.0):
|
||||
- FirebaseCoreInternal (~> 11.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.5.0):
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -102,32 +103,41 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.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
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.12.2):
|
||||
- flutter_webrtc (0.12.6):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.4.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -163,10 +173,16 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (8.0.2):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- home_widget (0.0.1):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- livekit_client (2.3.1):
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.1.3)
|
||||
- livekit_client (2.3.5):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
@ -179,6 +195,7 @@ PODS:
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- pasteboard (0.0.1):
|
||||
@ -189,17 +206,14 @@ PODS:
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- receive_sharing_intent (1.8.1):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.20.0):
|
||||
- SDWebImage/Core (= 5.20.0)
|
||||
- SDWebImage/Core (5.20.0)
|
||||
- Sentry/HybridSDK (8.40.1)
|
||||
- sentry_flutter (8.10.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.40.1)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@ -211,27 +225,37 @@ PODS:
|
||||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_compress (0.3.0):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- WebRTC-SDK (125.6422.06)
|
||||
- workmanager (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- Kingfisher (~> 8.0)
|
||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
@ -240,17 +264,20 @@ DEPENDENCIES:
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- 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`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Alamofire
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- Firebase
|
||||
@ -262,11 +289,12 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- Kingfisher
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- WebRTC-SDK
|
||||
|
||||
@ -275,12 +303,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
croppy:
|
||||
:path: ".symlinks/plugins/croppy/ios"
|
||||
cupertino_http:
|
||||
:path: ".symlinks/plugins/cupertino_http/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
file_saver:
|
||||
:path: ".symlinks/plugins/file_saver/ios"
|
||||
firebase_analytics:
|
||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||
firebase_core:
|
||||
@ -289,6 +317,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/firebase_messaging/ios"
|
||||
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:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_udid:
|
||||
@ -297,8 +329,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/darwin"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
livekit_client:
|
||||
:path: ".symlinks/plugins/livekit_client/ios"
|
||||
media_kit_libs_ios_video:
|
||||
@ -315,10 +351,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@ -327,61 +363,73 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_compress:
|
||||
:path: ".symlinks/plugins/video_compress/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
workmanager:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
||||
cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
|
||||
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
|
||||
firebase_core: 418aed674e9a0b8b6088aec16cde82a811f6261f
|
||||
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
|
||||
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
|
||||
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
|
||||
FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604
|
||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
||||
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
|
||||
firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
|
||||
firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
livekit_client: dbb906ef427fe96dde5854471c3dda0a50cc15f9
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
|
||||
Sentry: e9215d7b17f7902692b4f8700e061e4f853e3521
|
||||
sentry_flutter: 927eed60d66951d1b0f1db37fe94ff5cb7c80231
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: d2bdaa1cc7915e14cf47235c34a21fcb07b00390
|
||||
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
@ -11,6 +11,11 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */; };
|
||||
738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */; };
|
||||
738C1EB82D0D76A500A215F3 /* SolarWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 738C1EAB2D0D76A400A215F3 /* SolarWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7396A3522D16BD890095F4A8 /* NotifyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7396A3512D16BD890095F4A8 /* NotifyDelegate.swift */; };
|
||||
73B7746E2D0E869200A789CE /* SolarShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73B774642D0E869200A789CE /* SolarShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
73DA8A012D05C7620024A03E /* SolarNotifyService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73DA89FA2D05C7620024A03E /* SolarNotifyService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
8CD0929C27BC410DD5056EAB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A2C24C5238FAC44EA2CCF738 /* GoogleService-Info.plist */; };
|
||||
@ -18,6 +23,9 @@
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
CED170BFB6A72CDDAC285637 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */; };
|
||||
D5125CF12F159F0B8BC7641D /* Pods_SolarNotifyService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02469D286F48D84300484B1E /* Pods_SolarNotifyService.framework */; };
|
||||
D962B51F682FBDEC00AC7281 /* Pods_SolarWidgetExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B1A159F5551E280D0EFC129 /* Pods_SolarWidgetExtension.framework */; };
|
||||
F51C4E3C8FA95426C91FC0A4 /* Pods_SolarShare.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -28,6 +36,20 @@
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
738C1EB62D0D76A500A215F3 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 738C1EAA2D0D76A400A215F3;
|
||||
remoteInfo = SolarWidgetExtension;
|
||||
};
|
||||
73B7746C2D0E869200A789CE /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 73B774632D0E869200A789CE;
|
||||
remoteInfo = SolarShare;
|
||||
};
|
||||
73DA89FF2D05C7620024A03E /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
@ -40,10 +62,12 @@
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
73DA8A022D05C7620024A03E /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 12;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
738C1EB82D0D76A500A215F3 /* SolarWidgetExtension.appex in Embed Foundation Extensions */,
|
||||
73B7746E2D0E869200A789CE /* SolarShare.appex in Embed Foundation Extensions */,
|
||||
73DA8A012D05C7620024A03E /* SolarNotifyService.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
@ -62,22 +86,40 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
02469D286F48D84300484B1E /* Pods_SolarNotifyService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolarNotifyService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1077EFD9ACF793E9DA5D5B63 /* Pods-Runner-SolarNotifyService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarNotifyService.release.xcconfig"; path = "Target Support Files/Pods-Runner-SolarNotifyService/Pods-Runner-SolarNotifyService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolarShare.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2134F3903A0E8EB8CC2670BE /* Pods-SolarWidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarWidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolarWidgetExtension/Pods-SolarWidgetExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
26CC8DE2338798EAB472B62D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2DA1B873D39B9FD33298BBCE /* Pods-SolarShare.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarShare.profile.xcconfig"; path = "Target Support Files/Pods-SolarShare/Pods-SolarShare.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
40B53769EB464E54DACA7CE4 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
430F31F96B82659CBEAD4326 /* Pods-Runner-SolarWidgetExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarWidgetExtension.profile.xcconfig"; path = "Target Support Files/Pods-Runner-SolarWidgetExtension/Pods-Runner-SolarWidgetExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
48AE73F9950AF4FB02B5E9F4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
4A2F84B6033057E3BD2C7CB8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
4CBF45ABD292EE527D0A4D1E /* Pods-SolarNotifyService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarNotifyService.profile.xcconfig"; path = "Target Support Files/Pods-SolarNotifyService/Pods-SolarNotifyService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
5922A50B1231B06B92E31F20 /* Pods-SolarShare.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarShare.debug.xcconfig"; path = "Target Support Files/Pods-SolarShare/Pods-SolarShare.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
6618E2E3015264643175B43D /* Pods-SolarWidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarWidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-SolarWidgetExtension/Pods-SolarWidgetExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
72E9279EFA6DAC00BBAC493C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
73111C212CEE3D5E004CF4B3 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
738C1EAB2D0D76A400A215F3 /* SolarWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolarWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
738C1F132D0D7DDC00A215F3 /* SolarWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SolarWidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
7396A3512D16BD890095F4A8 /* NotifyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyDelegate.swift; sourceTree = "<group>"; };
|
||||
73B774642D0E869200A789CE /* SolarShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolarShare.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
73DA89FA2D05C7620024A03E /* SolarNotifyService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolarNotifyService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7B1A159F5551E280D0EFC129 /* Pods_SolarWidgetExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolarWidgetExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8E44A071621D5CAF864FB2F1 /* Pods-Runner-SolarNotifyService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarNotifyService.debug.xcconfig"; path = "Target Support Files/Pods-Runner-SolarNotifyService/Pods-Runner-SolarNotifyService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
931FBE9EDB99B3AD8B1FFB00 /* Pods-Runner-SolarWidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarWidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-Runner-SolarWidgetExtension/Pods-Runner-SolarWidgetExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@ -87,11 +129,53 @@
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; 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>"; };
|
||||
B1763F1D7318A2745CA7EDFE /* Pods-SolarShare.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarShare.release.xcconfig"; path = "Target Support Files/Pods-SolarShare/Pods-SolarShare.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B4550C68292419CDC580808B /* Pods-Runner-SolarNotifyService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarNotifyService.profile.xcconfig"; path = "Target Support Files/Pods-Runner-SolarNotifyService/Pods-Runner-SolarNotifyService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
BCE0C4086B776A27B202B373 /* Pods-SolarWidgetExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarWidgetExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolarWidgetExtension/Pods-SolarWidgetExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
BFF3B436D74FA8CBFFE34A27 /* Pods-Runner-SolarWidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarWidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-Runner-SolarWidgetExtension/Pods-Runner-SolarWidgetExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
D7E1FA77FDA53439DB2C0E75 /* Pods-SolarNotifyService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarNotifyService.release.xcconfig"; path = "Target Support Files/Pods-SolarNotifyService/Pods-SolarNotifyService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
D96D1DB4ED46A2640C1B9D34 /* Pods-SolarNotifyService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarNotifyService.debug.xcconfig"; path = "Target Support Files/Pods-SolarNotifyService/Pods-SolarNotifyService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
73DA8A062D05C7620024A03E /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
738C1EB92D0D76A500A215F3 /* Exceptions for "SolarWidget" folder in "SolarWidgetExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */;
|
||||
};
|
||||
738C1F512D0D91D000A215F3 /* Exceptions for "Data" folder in "SolarWidgetExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Post.swift,
|
||||
User.swift,
|
||||
);
|
||||
target = 738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */;
|
||||
};
|
||||
73B774722D0E869200A789CE /* Exceptions for "SolarShare" folder in "SolarShare" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 73B774632D0E869200A789CE /* SolarShare */;
|
||||
};
|
||||
73BC73712D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarNotifyService" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Attachment.swift,
|
||||
);
|
||||
target = 73DA89F92D05C7620024A03E /* SolarNotifyService */;
|
||||
};
|
||||
73BC73722D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarWidgetExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Attachment.swift,
|
||||
);
|
||||
target = 738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */;
|
||||
};
|
||||
73DA8A062D05C7620024A03E /* Exceptions for "SolarNotifyService" folder in "SolarNotifyService" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
@ -101,14 +185,73 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
73DA89FB2D05C7620024A03E /* SolarNotifyService */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (73DA8A062D05C7620024A03E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SolarNotifyService; sourceTree = "<group>"; };
|
||||
738C1EAE2D0D76A400A215F3 /* SolarWidget */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
738C1EB92D0D76A500A215F3 /* Exceptions for "SolarWidget" folder in "SolarWidgetExtension" target */,
|
||||
);
|
||||
path = SolarWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
738C1F4F2D0D91CC00A215F3 /* Data */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
738C1F512D0D91D000A215F3 /* Exceptions for "Data" folder in "SolarWidgetExtension" target */,
|
||||
);
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73B774652D0E869200A789CE /* SolarShare */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
73B774722D0E869200A789CE /* Exceptions for "SolarShare" folder in "SolarShare" target */,
|
||||
);
|
||||
path = SolarShare;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73BC736C2D0DDF5600956BE0 /* Service */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
73BC73712D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarNotifyService" target */,
|
||||
73BC73722D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarWidgetExtension" target */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73DA89FB2D05C7620024A03E /* SolarNotifyService */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
73DA8A062D05C7620024A03E /* Exceptions for "SolarNotifyService" folder in "SolarNotifyService" target */,
|
||||
);
|
||||
path = SolarNotifyService;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
738C1EA82D0D76A400A215F3 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */,
|
||||
738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */,
|
||||
D962B51F682FBDEC00AC7281 /* Pods_SolarWidgetExtension.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73B774612D0E869200A789CE /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F51C4E3C8FA95426C91FC0A4 /* Pods_SolarShare.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73DA89F72D05C7620024A03E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D5125CF12F159F0B8BC7641D /* Pods_SolarNotifyService.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -136,6 +279,11 @@
|
||||
children = (
|
||||
EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */,
|
||||
26CC8DE2338798EAB472B62D /* Pods_RunnerTests.framework */,
|
||||
731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */,
|
||||
731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */,
|
||||
16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */,
|
||||
02469D286F48D84300484B1E /* Pods_SolarNotifyService.framework */,
|
||||
7B1A159F5551E280D0EFC129 /* Pods_SolarWidgetExtension.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -162,9 +310,12 @@
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
738C1F132D0D7DDC00A215F3 /* SolarWidgetExtension.entitlements */,
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
73DA89FB2D05C7620024A03E /* SolarNotifyService */,
|
||||
738C1EAE2D0D76A400A215F3 /* SolarWidget */,
|
||||
73B774652D0E869200A789CE /* SolarShare */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
F5165E3BD1F2519F85CD4BE2 /* Pods */,
|
||||
@ -179,6 +330,8 @@
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
73DA89FA2D05C7620024A03E /* SolarNotifyService.appex */,
|
||||
738C1EAB2D0D76A400A215F3 /* SolarWidgetExtension.appex */,
|
||||
73B774642D0E869200A789CE /* SolarShare.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -186,6 +339,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
73BC736C2D0DDF5600956BE0 /* Service */,
|
||||
738C1F4F2D0D91CC00A215F3 /* Data */,
|
||||
73111C212CEE3D5E004CF4B3 /* Runner.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
@ -195,6 +350,7 @@
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
7396A3512D16BD890095F4A8 /* NotifyDelegate.swift */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
@ -208,6 +364,21 @@
|
||||
40B53769EB464E54DACA7CE4 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */,
|
||||
96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */,
|
||||
5922A50B1231B06B92E31F20 /* Pods-SolarShare.debug.xcconfig */,
|
||||
B1763F1D7318A2745CA7EDFE /* Pods-SolarShare.release.xcconfig */,
|
||||
2DA1B873D39B9FD33298BBCE /* Pods-SolarShare.profile.xcconfig */,
|
||||
2134F3903A0E8EB8CC2670BE /* Pods-SolarWidgetExtension.debug.xcconfig */,
|
||||
6618E2E3015264643175B43D /* Pods-SolarWidgetExtension.release.xcconfig */,
|
||||
BCE0C4086B776A27B202B373 /* Pods-SolarWidgetExtension.profile.xcconfig */,
|
||||
D96D1DB4ED46A2640C1B9D34 /* Pods-SolarNotifyService.debug.xcconfig */,
|
||||
D7E1FA77FDA53439DB2C0E75 /* Pods-SolarNotifyService.release.xcconfig */,
|
||||
4CBF45ABD292EE527D0A4D1E /* Pods-SolarNotifyService.profile.xcconfig */,
|
||||
8E44A071621D5CAF864FB2F1 /* Pods-Runner-SolarNotifyService.debug.xcconfig */,
|
||||
1077EFD9ACF793E9DA5D5B63 /* Pods-Runner-SolarNotifyService.release.xcconfig */,
|
||||
B4550C68292419CDC580808B /* Pods-Runner-SolarNotifyService.profile.xcconfig */,
|
||||
BFF3B436D74FA8CBFFE34A27 /* Pods-Runner-SolarWidgetExtension.debug.xcconfig */,
|
||||
931FBE9EDB99B3AD8B1FFB00 /* Pods-Runner-SolarWidgetExtension.release.xcconfig */,
|
||||
430F31F96B82659CBEAD4326 /* Pods-Runner-SolarWidgetExtension.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@ -234,10 +405,54 @@
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 738C1EBA2D0D76A500A215F3 /* Build configuration list for PBXNativeTarget "SolarWidgetExtension" */;
|
||||
buildPhases = (
|
||||
F2FCDA0E1BD434BF4883AFFD /* [CP] Check Pods Manifest.lock */,
|
||||
738C1EA72D0D76A400A215F3 /* Sources */,
|
||||
738C1EA82D0D76A400A215F3 /* Frameworks */,
|
||||
738C1EA92D0D76A400A215F3 /* Resources */,
|
||||
738C1EBE2D0D76C500A215F3 /* Copy Bundle Version */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
738C1EAE2D0D76A400A215F3 /* SolarWidget */,
|
||||
);
|
||||
name = SolarWidgetExtension;
|
||||
productName = SolarWidgetExtension;
|
||||
productReference = 738C1EAB2D0D76A400A215F3 /* SolarWidgetExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
73B774632D0E869200A789CE /* SolarShare */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 73B774732D0E869200A789CE /* Build configuration list for PBXNativeTarget "SolarShare" */;
|
||||
buildPhases = (
|
||||
9E6442609CE65E253572BC79 /* [CP] Check Pods Manifest.lock */,
|
||||
73B774602D0E869200A789CE /* Sources */,
|
||||
73B774612D0E869200A789CE /* Frameworks */,
|
||||
73B774622D0E869200A789CE /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
73B774652D0E869200A789CE /* SolarShare */,
|
||||
);
|
||||
name = SolarShare;
|
||||
productName = SolarShare;
|
||||
productReference = 73B774642D0E869200A789CE /* SolarShare.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
73DA89F92D05C7620024A03E /* SolarNotifyService */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 73DA8A072D05C7620024A03E /* Build configuration list for PBXNativeTarget "SolarNotifyService" */;
|
||||
buildPhases = (
|
||||
50F5704AB2E7309C916CA2E7 /* [CP] Check Pods Manifest.lock */,
|
||||
73DA89F62D05C7620024A03E /* Sources */,
|
||||
73DA89F72D05C7620024A03E /* Frameworks */,
|
||||
73DA89F82D05C7620024A03E /* Resources */,
|
||||
@ -250,8 +465,6 @@
|
||||
73DA89FB2D05C7620024A03E /* SolarNotifyService */,
|
||||
);
|
||||
name = SolarNotifyService;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = SolarNotifyService;
|
||||
productReference = 73DA89FA2D05C7620024A03E /* SolarNotifyService.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
@ -276,6 +489,12 @@
|
||||
);
|
||||
dependencies = (
|
||||
73DA8A002D05C7620024A03E /* PBXTargetDependency */,
|
||||
738C1EB72D0D76A500A215F3 /* PBXTargetDependency */,
|
||||
73B7746D2D0E869200A789CE /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
738C1F4F2D0D91CC00A215F3 /* Data */,
|
||||
73BC736C2D0DDF5600956BE0 /* Service */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
@ -289,7 +508,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1610;
|
||||
LastSwiftUpdateCheck = 1620;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
@ -297,6 +516,12 @@
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
738C1EAA2D0D76A400A215F3 = {
|
||||
CreatedOnToolsVersion = 16.2;
|
||||
};
|
||||
73B774632D0E869200A789CE = {
|
||||
CreatedOnToolsVersion = 16.2;
|
||||
};
|
||||
73DA89F92D05C7620024A03E = {
|
||||
CreatedOnToolsVersion = 16.1;
|
||||
};
|
||||
@ -307,7 +532,6 @@
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
@ -315,6 +539,7 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@ -322,6 +547,8 @@
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
73DA89F92D05C7620024A03E /* SolarNotifyService */,
|
||||
738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */,
|
||||
73B774632D0E869200A789CE /* SolarShare */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@ -334,6 +561,20 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
738C1EA92D0D76A400A215F3 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73B774622D0E869200A789CE /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73DA89F82D05C7620024A03E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -388,7 +629,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
|
||||
};
|
||||
43B5CF57FD79BC21654EE037 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@ -407,6 +648,46 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
50F5704AB2E7309C916CA2E7 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-SolarNotifyService-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
738C1EBE2D0D76C500A215F3 /* Copy Bundle Version */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Copy Bundle Version";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "generatedPath=\"$SRCROOT/Flutter/Generated.xcconfig\"\n\n# Read and trim versionNumber and buildNumber\nversionNumber=$(grep FLUTTER_BUILD_NAME \"$generatedPath\" | cut -d '=' -f2 | xargs)\nbuildNumber=$(grep FLUTTER_BUILD_NUMBER \"$generatedPath\" | cut -d '=' -f2 | xargs)\n\ninfoPlistPath=\"$SRCROOT/HomeExampleWidget/Info.plist\"\n\n# Check and add CFBundleVersion if it does not exist\n/usr/libexec/PlistBuddy -c \"Print :CFBundleVersion\" \"$infoPlistPath\" 2>/dev/null\nif [ $? != 0 ]; then\n /usr/libexec/PlistBuddy -c \"Add :CFBundleVersion string $buildNumber\" \"$infoPlistPath\"\nelse\n /usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"$infoPlistPath\"\nfi\n\n# Check and add CFBundleShortVersionString if it does not exist\n/usr/libexec/PlistBuddy -c \"Print :CFBundleShortVersionString\" \"$infoPlistPath\" 2>/dev/null\nif [ $? != 0 ]; then\n /usr/libexec/PlistBuddy -c \"Add :CFBundleShortVersionString string $versionNumber\" \"$infoPlistPath\"\nelse\n /usr/libexec/PlistBuddy -c \"Set :CFBundleShortVersionString $versionNumber\" \"$infoPlistPath\"\nfi\n\n";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@ -422,6 +703,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
9E6442609CE65E253572BC79 /* [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-SolarShare-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;
|
||||
};
|
||||
C431F2F1BD10FD03D14DDAE1 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -466,6 +769,28 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F2FCDA0E1BD434BF4883AFFD /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-SolarWidgetExtension-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FC4815D44D909666EB1FA614 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -494,6 +819,20 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
738C1EA72D0D76A400A215F3 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73B774602D0E869200A789CE /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73DA89F62D05C7620024A03E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -507,6 +846,7 @@
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7396A3522D16BD890095F4A8 /* NotifyDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -518,6 +858,16 @@
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
738C1EB72D0D76A500A215F3 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */;
|
||||
targetProxy = 738C1EB62D0D76A500A215F3 /* PBXContainerItemProxy */;
|
||||
};
|
||||
73B7746D2D0E869200A789CE /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 73B774632D0E869200A789CE /* SolarShare */;
|
||||
targetProxy = 73B7746C2D0E869200A789CE /* PBXContainerItemProxy */;
|
||||
};
|
||||
73DA8A002D05C7620024A03E /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 73DA89F92D05C7620024A03E /* SolarNotifyService */;
|
||||
@ -605,11 +955,13 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -629,6 +981,7 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||
@ -647,6 +1000,7 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||
@ -663,6 +1017,7 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||
@ -672,8 +1027,135 @@
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
73DA8A032D05C7620024A03E /* Debug */ = {
|
||||
738C1EBB2D0D76A500A215F3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 2134F3903A0E8EB8CC2670BE /* Pods-SolarWidgetExtension.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolarWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolarWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
738C1EBC2D0D76A500A215F3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6618E2E3015264643175B43D /* Pods-SolarWidgetExtension.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolarWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolarWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
738C1EBD2D0D76A500A215F3 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BCE0C4086B776A27B202B373 /* Pods-SolarWidgetExtension.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolarWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolarWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
73B7746F2D0E869200A789CE /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5922A50B1231B06B92E31F20 /* Pods-SolarShare.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@ -682,6 +1164,130 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarShare/SolarShare.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolarShare/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarShare;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolarShare;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
73B774702D0E869200A789CE /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B1763F1D7318A2745CA7EDFE /* Pods-SolarShare.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarShare/SolarShare.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolarShare/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarShare;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolarShare;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
73B774712D0E869200A789CE /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 2DA1B873D39B9FD33298BBCE /* Pods-SolarShare.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarShare/SolarShare.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolarShare/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarShare;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolarShare;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
73DA8A032D05C7620024A03E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D96D1DB4ED46A2640C1B9D34 /* Pods-SolarNotifyService.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarNotifyService/SolarNotifyService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
@ -691,7 +1297,7 @@
|
||||
INFOPLIST_FILE = SolarNotifyService/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarNotifyService;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -714,6 +1320,7 @@
|
||||
};
|
||||
73DA8A042D05C7620024A03E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D7E1FA77FDA53439DB2C0E75 /* Pods-SolarNotifyService.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@ -722,6 +1329,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarNotifyService/SolarNotifyService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
@ -731,7 +1339,7 @@
|
||||
INFOPLIST_FILE = SolarNotifyService/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarNotifyService;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -751,6 +1359,7 @@
|
||||
};
|
||||
73DA8A052D05C7620024A03E /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4CBF45ABD292EE527D0A4D1E /* Pods-SolarNotifyService.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@ -759,6 +1368,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolarNotifyService/SolarNotifyService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
@ -768,7 +1378,7 @@
|
||||
INFOPLIST_FILE = SolarNotifyService/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolarNotifyService;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -905,11 +1515,13 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -931,11 +1543,13 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -961,6 +1575,26 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
738C1EBA2D0D76A500A215F3 /* Build configuration list for PBXNativeTarget "SolarWidgetExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
738C1EBB2D0D76A500A215F3 /* Debug */,
|
||||
738C1EBC2D0D76A500A215F3 /* Release */,
|
||||
738C1EBD2D0D76A500A215F3 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
73B774732D0E869200A789CE /* Build configuration list for PBXNativeTarget "SolarShare" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
73B7746F2D0E869200A789CE /* Debug */,
|
||||
73B774702D0E869200A789CE /* Release */,
|
||||
73B774712D0E869200A789CE /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
73DA8A072D05C7620024A03E /* Build configuration list for PBXNativeTarget "SolarNotifyService" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
@ -1,13 +1,26 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
import workmanager
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
let notifyDelegate = NotifyDelegate()
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
|
||||
GeneratedPluginRegistrant.register(with: registry)
|
||||
}
|
||||
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60*5))
|
||||
|
||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
}
|
||||
|
39
ios/Runner/AppIntent.swift
Normal file
@ -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 {}
|
38
ios/Runner/Data/Post.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// SolarPost.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SolarPost : Codable {
|
||||
let id: Int
|
||||
let body: SolarPostBody
|
||||
let publisher: SolarPublisher
|
||||
let publisherId: Int
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let editedAt: Date?
|
||||
let publishedAt: Date?
|
||||
}
|
||||
|
||||
struct SolarPostBody : Codable {
|
||||
let content: String?
|
||||
let title: String?
|
||||
let description: String?
|
||||
let attachments: [String]?
|
||||
}
|
||||
|
||||
struct SolarPublisher : Codable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let nick: String
|
||||
let description: String?
|
||||
let avatar: String?
|
||||
let banner: String?
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
}
|
21
ios/Runner/Data/User.swift
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// SolarData.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SolarUser: Codable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let nick: String
|
||||
}
|
||||
|
||||
struct SolarCheckInRecord: Codable {
|
||||
let id: Int
|
||||
let resultTier: Int
|
||||
let resultExperience: Int
|
||||
let createdAt: Date
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>group.solsynth.solian</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
@ -27,6 +29,17 @@
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
@ -34,13 +47,17 @@
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian take photo or video for your post.</string>
|
||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian record audio for your post.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
@ -62,10 +79,6 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
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()
|
||||
}
|
||||
}
|
@ -4,7 +4,16 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>webcredentials:sn.solsynth.dev</string>
|
||||
<string>applinks:sn.solsynth.dev</string>
|
||||
</array>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.solsynth.solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
14
ios/Runner/Service/Attachment.swift
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// Attachment.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func getAttachmentUrl(for identifier: String) -> String {
|
||||
let serverBaseUrl = "https://api.sn.solsynth.dev"
|
||||
|
||||
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/cgi/uc/attachments/\(identifier)"
|
||||
}
|
@ -7,6 +7,8 @@
|
||||
|
||||
import UserNotifications
|
||||
import Intents
|
||||
import Kingfisher
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
enum ParseNotificationPayloadError: Error {
|
||||
case missingMetadata(String)
|
||||
@ -17,11 +19,6 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
private var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
private var bestAttemptContent: UNMutableNotificationContent?
|
||||
private let serverBaseUrl = "https://api.sn.solsynth.dev"
|
||||
|
||||
private func getAttachmentUrl(for identifier: String) -> String {
|
||||
identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/cgi/uc/attachments/\(identifier)"
|
||||
}
|
||||
|
||||
override func didReceive(
|
||||
_ request: UNNotificationRequest,
|
||||
@ -61,33 +58,63 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
throw ParseNotificationPayloadError.missingMetadata("The notification has no metadata.")
|
||||
}
|
||||
|
||||
guard var avatarUrl = metadata["avatar"] as? String else {
|
||||
guard let avatarIdentifier = metadata["avatar"] as? String else {
|
||||
throw ParseNotificationPayloadError.missingAvatarUrl("The notification has no avatar.")
|
||||
}
|
||||
avatarUrl = getAttachmentUrl(for: avatarUrl)
|
||||
|
||||
let handle = INPersonHandle(value: "\(metadata["user_id"] ?? "")", type: .unknown)
|
||||
let avatar = INImage(url: URL(string: avatarUrl)!)
|
||||
let sender = INPerson(
|
||||
personHandle: handle,
|
||||
nameComponents: nil,
|
||||
displayName: content.title,
|
||||
image: avatar,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil
|
||||
let replyableMessageCategory = UNNotificationCategory(
|
||||
identifier: content.categoryIdentifier,
|
||||
actions: [
|
||||
UNTextInputNotificationAction(
|
||||
identifier: "reply_action",
|
||||
title: "Reply",
|
||||
options: []
|
||||
),
|
||||
],
|
||||
intentIdentifiers: [],
|
||||
options: []
|
||||
)
|
||||
|
||||
if content.categoryIdentifier == "messaging.callStart" {
|
||||
let intent = createCallIntent(with: sender)
|
||||
donateInteraction(for: intent)
|
||||
let updatedContent = try request.content.updating(from: intent)
|
||||
contentHandler?(updatedContent)
|
||||
} else {
|
||||
let intent = createMessageIntent(with: sender, metadata: metadata, body: content.body)
|
||||
donateInteraction(for: intent)
|
||||
let updatedContent = try request.content.updating(from: intent)
|
||||
contentHandler?(updatedContent)
|
||||
}
|
||||
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(
|
||||
personHandle: handle,
|
||||
nameComponents: nil,
|
||||
displayName: content.title,
|
||||
image: image == nil ? nil : INImage(imageData: image!),
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil
|
||||
)
|
||||
|
||||
if content.categoryIdentifier == "messaging.callStart" {
|
||||
let intent = self.createCallIntent(with: sender)
|
||||
self.donateInteraction(for: intent)
|
||||
let updatedContent = try? request.content.updating(from: intent)
|
||||
self.contentHandler?(updatedContent ?? content)
|
||||
} else {
|
||||
let intent = self.createMessageIntent(with: sender, metadata: metadataCopy, body: content.body)
|
||||
self.donateInteraction(for: intent)
|
||||
let updatedContent = try? request.content.updating(from: intent)
|
||||
self.contentHandler?(updatedContent ?? content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func handleDefaultNotification(content: UNMutableNotificationContent) throws {
|
||||
@ -96,19 +123,79 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
if let imageIdentifier = metadata["image"] as? String {
|
||||
attachMedia(to: content, withIdentifier: imageIdentifier)
|
||||
attachMedia(to: content, withIdentifier: imageIdentifier, fileType: UTType.jpeg, doScaleDown: true)
|
||||
} else if let avatarIdentifier = metadata["avatar"] as? String {
|
||||
attachMedia(to: content, withIdentifier: avatarIdentifier)
|
||||
attachMedia(to: content, withIdentifier: avatarIdentifier, fileType: UTType.jpeg, doScaleDown: true)
|
||||
} else {
|
||||
contentHandler?(content)
|
||||
}
|
||||
|
||||
contentHandler?(content)
|
||||
}
|
||||
|
||||
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String) {
|
||||
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
|
||||
let attachmentUrl = getAttachmentUrl(for: identifier)
|
||||
if let url = URL(string: attachmentUrl), let attachment = try? UNNotificationAttachment(identifier: identifier, url: url) {
|
||||
content.attachments = [attachment]
|
||||
|
||||
guard let remoteUrl = URL(string: attachmentUrl) else {
|
||||
print("Invalid URL for attachment: \(attachmentUrl)")
|
||||
return
|
||||
}
|
||||
|
||||
let targetSize = 800
|
||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
||||
.processor(scaleProcessor)
|
||||
] : nil) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let retrievalResult):
|
||||
// The image is either retrieved from cache or downloaded
|
||||
let tempDirectory = FileManager.default.temporaryDirectory
|
||||
let cachedFileUrl = tempDirectory.appendingPathComponent(identifier)
|
||||
|
||||
do {
|
||||
// Write the image data to a temporary file for UNNotificationAttachment
|
||||
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
||||
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: identifier)
|
||||
} catch {
|
||||
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
print("Failed to retrieve image: \(error.localizedDescription)")
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) {
|
||||
do {
|
||||
let attachment = try UNNotificationAttachment(identifier: identifier, url: localUrl, options: [
|
||||
UNNotificationAttachmentOptionsTypeHintKey: type as Any,
|
||||
UNNotificationAttachmentOptionsThumbnailHiddenKey: 0,
|
||||
])
|
||||
content.attachments = [attachment]
|
||||
} catch let error as NSError {
|
||||
// Log detailed error information
|
||||
print("Failed to create attachment from file at \(localUrl.path)")
|
||||
print("Error: \(error.localizedDescription)")
|
||||
|
||||
// Check specific error codes if needed
|
||||
if error.domain == NSCocoaErrorDomain {
|
||||
switch error.code {
|
||||
case NSFileReadNoSuchFileError:
|
||||
print("File does not exist at \(localUrl.path)")
|
||||
case NSFileReadNoPermissionError:
|
||||
print("No permission to read file at \(localUrl.path)")
|
||||
default:
|
||||
print("Unhandled file error: \(error.code)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call content handler regardless of success or failure
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
|
||||
private func createCallIntent(with sender: INPerson) -> INStartCallIntent {
|
||||
|
10
ios/SolarNotifyService/SolarNotifyService.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.solsynth.solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
24
ios/SolarShare/Base.lproj/MainInterface.storyboard
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Share View Controller-->
|
||||
<scene sceneID="ceB-am-kn3">
|
||||
<objects>
|
||||
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
36
ios/SolarShare/Info.plist
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PHSupportedMediaTypes</key>
|
||||
<array>
|
||||
<string>Video</string>
|
||||
<string>Image</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>15</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>15</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>15</integer>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
</dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>group.solsynth.solian</string>
|
||||
</dict>
|
||||
</plist>
|
18
ios/SolarShare/ShareViewController.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// SolarShare
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/15.
|
||||
//
|
||||
|
||||
import receive_sharing_intent
|
||||
|
||||
class ShareViewController: RSIShareViewController {
|
||||
|
||||
// Use this method to return false if you don't want to redirect to host app automatically.
|
||||
// Default is true
|
||||
override func shouldAutoRedirect() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
10
ios/SolarShare/SolarShare.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.solsynth.solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
ios/SolarWidget/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
135
ios/SolarWidget/CheckInWidget.swift
Normal file
@ -0,0 +1,135 @@
|
||||
//
|
||||
// SolarWidget.swift
|
||||
// SolarWidget
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct CheckInProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> CheckInEntry {
|
||||
CheckInEntry(date: Date(), checkIn: nil)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
||||
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 checkInRaw = prefs?.string(forKey: "pas_check_in_record")
|
||||
var checkIn: SolarCheckInRecord?
|
||||
if let checkInRaw = checkInRaw {
|
||||
checkIn = try! jsonDecoder.decode(SolarCheckInRecord.self, from: checkInRaw.data(using: .utf8)!)
|
||||
if checkIn != nil && !Calendar.current.isDate(checkIn!.createdAt, inSameDayAs: Date()) {
|
||||
checkIn = nil
|
||||
}
|
||||
}
|
||||
|
||||
let entry = CheckInEntry(
|
||||
date: Date(),
|
||||
checkIn: checkIn
|
||||
)
|
||||
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 CheckInEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let checkIn: SolarCheckInRecord?
|
||||
}
|
||||
|
||||
struct CheckInWidgetEntryView : View {
|
||||
var entry: CheckInProvider.Entry
|
||||
|
||||
private let resultTierSymbols: [String] = ["大凶", "凶", "中平", "吉", "大吉"]
|
||||
|
||||
func checkIn() -> Void {}
|
||||
|
||||
func seeDetail() -> Void {}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if let checkIn = entry.checkIn {
|
||||
VStack(alignment: .leading) {
|
||||
Text(resultTierSymbols[checkIn.resultTier]).font(.system(size: 27, weight: .bold))
|
||||
Text("+\(checkIn.resultExperience) EXP").font(.system(size: 15, design: .monospaced))
|
||||
}.padding(.horizontal, 4)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(
|
||||
checkIn.createdAt,
|
||||
format: .dateTime.weekday()
|
||||
).font(.system(size: 13))
|
||||
Text(
|
||||
checkIn.createdAt,
|
||||
format: .dateTime.day().month()
|
||||
).font(.system(size: 13))
|
||||
}.padding(.leading, 4)
|
||||
|
||||
Button("See Detail", systemImage: "arrow.right", action: seeDetail)
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonBorderShape(.circle)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}.frame(alignment: .bottom)
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Check In").font(.system(size: 19, weight: .bold))
|
||||
Text("You haven't check in today").font(.system(size: 15))
|
||||
}.padding(.horizontal, 4)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}.padding(8).widgetURL(URL(string: "https://sn.solsynth.dev"))
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckInWidget: Widget {
|
||||
let kind: String = "SolarCheckInWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CheckInProvider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
CheckInWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
CheckInWidgetEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("Check In")
|
||||
.description("View your today's fortune on your home screen")
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
CheckInWidget()
|
||||
} timeline: {
|
||||
CheckInEntry(date: .now, checkIn: nil)
|
||||
CheckInEntry(
|
||||
date: .now,
|
||||
checkIn: SolarCheckInRecord(id: 1, resultTier: 1, resultExperience: 100, createdAt: Date.now)
|
||||
)
|
||||
}
|
11
ios/SolarWidget/Info.plist
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
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
|
||||
)
|
||||
}
|
17
ios/SolarWidget/SolarWidgetBundle.swift
Normal file
@ -0,0 +1,17 @@
|
||||
//
|
||||
// SolarWidgetBundle.swift
|
||||
// SolarWidget
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SolarWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
CheckInWidget()
|
||||
RandomPostWidget()
|
||||
}
|
||||
}
|
10
ios/SolarWidgetExtension.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.solsynth.solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
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/websocket.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/websocket.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatMessageController extends ChangeNotifier {
|
||||
@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
|
||||
int? messageTotal;
|
||||
|
||||
bool get isAllLoaded =>
|
||||
messageTotal != null && messages.length >= messageTotal!;
|
||||
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
|
||||
|
||||
String? _boxKey;
|
||||
SnChannel? channel;
|
||||
@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
|
||||
/// Stored as a list of nonce to provide the loading state
|
||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||
|
||||
Box<SnChatMessage>? get _box =>
|
||||
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||
Box<SnChatMessage>? get _box => (_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 {
|
||||
channel = chan;
|
||||
@ -71,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
case 'events.new':
|
||||
if (event.payload?['channel_id'] != channel?.id) break;
|
||||
final payload = SnChatMessage.fromJson(event.payload!);
|
||||
_addMessage(payload);
|
||||
break;
|
||||
@ -78,22 +82,16 @@ class ChatMessageController extends ChangeNotifier {
|
||||
if (event.payload?['channel_id'] != channel?.id) break;
|
||||
final member = SnChannelMember.fromJson(event.payload!['member']);
|
||||
if (member.id == profile?.id) break;
|
||||
// TODO impl typing users
|
||||
// if (!_typingUsers.any((x) => x.id == member.id)) {
|
||||
// setState(() {
|
||||
// _typingUsers.add(member);
|
||||
// });
|
||||
// }
|
||||
// _typingInactiveTimer[member.id]?.cancel();
|
||||
// _typingInactiveTimer[member.id] = Timer(
|
||||
// const Duration(seconds: 3),
|
||||
// () {
|
||||
// setState(() {
|
||||
// _typingUsers.removeWhere((x) => x.id == member.id);
|
||||
// _typingInactiveTimer.remove(member.id);
|
||||
// });
|
||||
// },
|
||||
// );
|
||||
if (!typingMembers.any((x) => x.id == member.id)) {
|
||||
typingMembers.add(member);
|
||||
notifyListeners();
|
||||
}
|
||||
typingInactiveTimer[member.id]?.cancel();
|
||||
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
|
||||
typingMembers.removeWhere((x) => x.id == member.id);
|
||||
typingInactiveTimer.remove(member.id);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
|
||||
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 {
|
||||
if (_box == null) return;
|
||||
await _box!.putAll({
|
||||
@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
switch (message.type) {
|
||||
case 'messages.edit':
|
||||
if (message.relatedEventId != null) {
|
||||
final idx =
|
||||
messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||
if (idx != -1) {
|
||||
final newBody = message.body;
|
||||
newBody.remove('related_event');
|
||||
@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
'algorithm': 'plain',
|
||||
if (quoteId != null) 'quote_event': quoteId,
|
||||
if (relatedId != null) 'related_event': relatedId,
|
||||
if (attachments != null && attachments.isNotEmpty)
|
||||
'attachments': attachments,
|
||||
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
|
||||
};
|
||||
|
||||
// Mock the message locally
|
||||
@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
|
||||
if (out == null) {
|
||||
try {
|
||||
final resp = await _sn.client
|
||||
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||
out = SnChatMessage.fromJson(resp.data);
|
||||
_saveMessageToLocal([out]);
|
||||
} catch (_) {
|
||||
@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
bool forceRemote = false,
|
||||
}) async {
|
||||
late List<SnChatMessage> out;
|
||||
if (_box != null &&
|
||||
(_box!.length >= take + offset || forceLocal) &&
|
||||
!forceRemote) {
|
||||
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
|
||||
out = _box!.keys
|
||||
.toList()
|
||||
.cast<int>()
|
||||
@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
quoteEvent: quoteEvent,
|
||||
attachments: attachments
|
||||
.where(
|
||||
(ele) =>
|
||||
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Preload sender accounts
|
||||
final accountId = out
|
||||
.where((ele) => ele.sender.accountId >= 0)
|
||||
.map((ele) => ele.sender.accountId)
|
||||
.toSet();
|
||||
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
|
||||
await _ud.listAccount(accountId);
|
||||
|
||||
return out;
|
||||
|
@ -1,11 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
@ -13,17 +19,11 @@ import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
enum PostWriteMediaType {
|
||||
image,
|
||||
video,
|
||||
audio,
|
||||
file,
|
||||
}
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
class PostWriteMedia {
|
||||
late String name;
|
||||
late PostWriteMediaType type;
|
||||
late SnMediaType type;
|
||||
final SnAttachment? attachment;
|
||||
final XFile? file;
|
||||
final Uint8List? raw;
|
||||
@ -35,16 +35,16 @@ class PostWriteMedia {
|
||||
|
||||
switch (attachment?.mimetype.split('/').firstOrNull) {
|
||||
case 'image':
|
||||
type = PostWriteMediaType.image;
|
||||
type = SnMediaType.image;
|
||||
break;
|
||||
case 'video':
|
||||
type = PostWriteMediaType.video;
|
||||
type = SnMediaType.video;
|
||||
break;
|
||||
case 'audio':
|
||||
type = PostWriteMediaType.audio;
|
||||
type = SnMediaType.audio;
|
||||
break;
|
||||
default:
|
||||
type = PostWriteMediaType.file;
|
||||
type = SnMediaType.file;
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,16 +56,16 @@ class PostWriteMedia {
|
||||
|
||||
switch (mimetype?.split('/').firstOrNull) {
|
||||
case 'image':
|
||||
type = PostWriteMediaType.image;
|
||||
type = SnMediaType.image;
|
||||
break;
|
||||
case 'video':
|
||||
type = PostWriteMediaType.video;
|
||||
type = SnMediaType.video;
|
||||
break;
|
||||
case 'audio':
|
||||
type = PostWriteMediaType.audio;
|
||||
type = SnMediaType.audio;
|
||||
break;
|
||||
default:
|
||||
type = PostWriteMediaType.file;
|
||||
type = SnMediaType.file;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ class PostWriteMedia {
|
||||
if (attachment != null) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||
if (width != null && height != null) {
|
||||
if (width != null && height != null && !kIsWeb) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
width: width,
|
||||
@ -152,10 +152,24 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController contentController = TextEditingController();
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
|
||||
PostWriteController() {
|
||||
titleController.addListener(() => notifyListeners());
|
||||
descriptionController.addListener(() => notifyListeners());
|
||||
bool _temporarySaveActive = false;
|
||||
|
||||
PostWriteController({bool doLoadFromTemporary = true}) {
|
||||
_temporarySaveActive = doLoadFromTemporary;
|
||||
titleController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
descriptionController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
contentController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
});
|
||||
if (doLoadFromTemporary) _temporaryLoad();
|
||||
}
|
||||
|
||||
String mode = kTitleMap.keys.first;
|
||||
@ -176,6 +190,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
List<int> visibleUsers = List.empty();
|
||||
List<int> invisibleUsers = List.empty();
|
||||
List<String> tags = List.empty();
|
||||
List<String> categories = List.empty();
|
||||
PostWriteMedia? thumbnail;
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
@ -198,12 +213,14 @@ class PostWriteController extends ChangeNotifier {
|
||||
titleController.text = post.body['title'] ?? '';
|
||||
descriptionController.text = post.body['description'] ?? '';
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
aliasController.text = post.alias ?? '';
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? []);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||
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)) ?? []);
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
@ -231,7 +248,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 place = await attach.chunkedUploadInitialize(
|
||||
@ -239,22 +257,141 @@ class PostWriteController extends ChangeNotifier {
|
||||
media.name,
|
||||
'interactive',
|
||||
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()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
progress = progress;
|
||||
analyzeNow: media.type == SnMediaType.image,
|
||||
onProgress: (value) {
|
||||
progress = value;
|
||||
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;
|
||||
}
|
||||
|
||||
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 (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(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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'] ?? '';
|
||||
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;
|
||||
temporaryRestored = true;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||
if (isBusy) return;
|
||||
|
||||
@ -269,7 +406,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> post(BuildContext context) async {
|
||||
Future<void> sendPost(BuildContext context) async {
|
||||
if (isBusy || publisher == null) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
@ -296,21 +433,34 @@ class PostWriteController extends ChangeNotifier {
|
||||
media.name,
|
||||
'interactive',
|
||||
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()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
onProgress: (value) {
|
||||
// Calculate overall progress for attachments
|
||||
progress = ((i + progress) / attachments.length) * kAttachmentProgressWeight;
|
||||
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
|
||||
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);
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (err) {
|
||||
isBusy = false;
|
||||
@ -334,11 +484,13 @@ class PostWriteController extends ChangeNotifier {
|
||||
data: {
|
||||
'publisher': publisher!.id,
|
||||
'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 (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
|
||||
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
@ -359,6 +511,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
reset();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -407,65 +560,88 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
void setPublisher(SnPublisher? item) {
|
||||
publisher = item;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedAt(DateTime? value) {
|
||||
publishedAt = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedUntil(DateTime? value) {
|
||||
publishedUntil = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setTags(List<String> value) {
|
||||
tags = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCategories(List<String> value) {
|
||||
categories = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibility(int value) {
|
||||
visibility = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibleUsers(List<int> value) {
|
||||
visibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setInvisibleUsers(List<int> value) {
|
||||
invisibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setProgress(double? value) {
|
||||
progress = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setIsBusy(bool value) {
|
||||
isBusy = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMode(String value) {
|
||||
mode = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
thumbnail = null;
|
||||
visibility = 0;
|
||||
titleController.clear();
|
||||
descriptionController.clear();
|
||||
contentController.clear();
|
||||
attachments.clear();
|
||||
aliasController.clear();
|
||||
tags = List.empty(growable: true);
|
||||
categories = List.empty(growable: true);
|
||||
attachments = List.empty(growable: true);
|
||||
editingPost = null;
|
||||
replyingPost = null;
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
temporaryRestored = false;
|
||||
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -474,6 +650,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.dispose();
|
||||
titleController.dispose();
|
||||
descriptionController.dispose();
|
||||
aliasController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
212
lib/main.dart
@ -1,7 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:croppy/croppy.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:easy_localization_loader/easy_localization_loader.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
@ -9,27 +12,54 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/firebase_options.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/link_preview.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/relationship.dart';
|
||||
import 'package:surface/providers/sn_attachment.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/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
import 'package:surface/router.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void appBackgroundDispatcher() {
|
||||
Workmanager().executeTask((task, inputData) async {
|
||||
log("[WorkManager] Native called background task: $task");
|
||||
switch (task) {
|
||||
case Workmanager.iOSBackgroundTask:
|
||||
await Future.wait([widgetUpdateRandomPost()]);
|
||||
return true;
|
||||
case "WidgetUpdateRandomPost":
|
||||
await widgetUpdateRandomPost();
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@ -57,15 +87,23 @@ void main() async {
|
||||
});
|
||||
}
|
||||
|
||||
await SentryFlutter.init(
|
||||
(options) {
|
||||
options.dsn =
|
||||
'https://c218d44126d59d69301e730498494def@o4506965897117696.ingest.us.sentry.io/4508346768228352';
|
||||
options.tracesSampleRate = 1.0;
|
||||
options.profilesSampleRate = 1.0;
|
||||
},
|
||||
appRunner: () => runApp(const SolianApp()),
|
||||
);
|
||||
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());
|
||||
}
|
||||
|
||||
class SolianApp extends StatelessWidget {
|
||||
@ -76,30 +114,45 @@ class SolianApp extends StatelessWidget {
|
||||
return ResponsiveBreakpoints.builder(
|
||||
child: EasyLocalization(
|
||||
path: 'assets/translations',
|
||||
supportedLocales: [Locale('en', 'US'), Locale('zh', 'CN')],
|
||||
supportedLocales: [
|
||||
Locale('en', 'US'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
Locale('zh', 'HK'),
|
||||
],
|
||||
fallbackLocale: Locale('en', 'US'),
|
||||
useFallbackTranslations: true,
|
||||
useOnlyLangCode: true,
|
||||
assetLoader: JsonAssetLoader(),
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
// System extensions layer
|
||||
Provider(create: (ctx) => HomeWidgetProvider(ctx)),
|
||||
|
||||
// Preferences layer
|
||||
ChangeNotifierProvider(create: (ctx) => ConfigProvider(ctx)),
|
||||
|
||||
// Display layer
|
||||
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
||||
ChangeNotifierProvider(create: (ctx) => NavigationProvider()),
|
||||
|
||||
// Data layer
|
||||
Provider(create: (_) => SnNetworkProvider()),
|
||||
Provider(create: (ctx) => SnNetworkProvider(ctx)),
|
||||
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
|
||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
||||
Provider(create: (ctx) => SnStickerProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
||||
|
||||
// Additional helper layer
|
||||
Provider(create: (ctx) => SpecialDayProvider(ctx)),
|
||||
],
|
||||
child: AppMainContent(),
|
||||
child: _AppDelegate(),
|
||||
),
|
||||
),
|
||||
breakpoints: [
|
||||
@ -111,8 +164,8 @@ class SolianApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AppMainContent extends StatelessWidget {
|
||||
const AppMainContent({super.key});
|
||||
class _AppDelegate extends StatelessWidget {
|
||||
const _AppDelegate();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -134,6 +187,133 @@ class AppMainContent extends StatelessWidget {
|
||||
...context.localizationDelegates,
|
||||
],
|
||||
routerConfig: appRouter,
|
||||
builder: (context, child) {
|
||||
return _AppSplashScreen(
|
||||
key: const Key('global-splash-screen'),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppSplashScreen extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const _AppSplashScreen({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
||||
}
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
void _tryRequestRating() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('first_boot_time')) {
|
||||
final rawTime = prefs.getString('first_boot_time');
|
||||
final time = DateTime.tryParse(rawTime ?? '');
|
||||
if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
|
||||
final inAppReview = InAppReview.instance;
|
||||
if (prefs.getBool('rating_requested') == true) return;
|
||||
if (await inAppReview.isAvailable()) {
|
||||
await inAppReview.requestReview();
|
||||
prefs.setBool('rating_requested', true);
|
||||
} else {
|
||||
log('Unable request app review, unavailable');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prefs.setString('first_boot_time', DateTime.now().toIso8601String());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkForUpdate() async {
|
||||
if (kIsWeb) return;
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||
final resp = await Dio(
|
||||
BaseOptions(
|
||||
sendTimeout: const Duration(seconds: 60),
|
||||
receiveTimeout: const Duration(seconds: 60),
|
||||
),
|
||||
).get(
|
||||
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
|
||||
);
|
||||
final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
|
||||
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
|
||||
final localVersion = Version.parse(localVersionString.split('+').first);
|
||||
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
||||
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
|
||||
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
||||
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
|
||||
final config = context.read<ConfigProvider>();
|
||||
config.setUpdate(remoteVersionString);
|
||||
log("[Update] Update available: $remoteVersionString");
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) context.showErrorDialog('Unable to check update: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
final home = context.read<HomeWidgetProvider>();
|
||||
await home.initialize();
|
||||
if (!mounted) return;
|
||||
// The Network initialization must be done after the HomeWidget initialization
|
||||
// The Network initialization will save the server url to the HomeWidget
|
||||
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.initializeUserAgent();
|
||||
await sn.setConfigWithNative();
|
||||
if (!mounted) return;
|
||||
final ua = context.read<UserProvider>();
|
||||
await ua.initialize();
|
||||
if (!mounted) return;
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
await ws.tryConnect();
|
||||
if (!mounted) return;
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _postInitialization() async {
|
||||
await widgetUpdateRandomPost();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initialize().then((_) {
|
||||
_postInitialization();
|
||||
_tryRequestRating();
|
||||
_checkForUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
return NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
return false;
|
||||
},
|
||||
child: SizeChangedLayoutNotifier(
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -125,10 +125,8 @@ class ChatChannelProvider extends ChangeNotifier {
|
||||
final channelBox = await Hive.openBox<SnChatMessage>(
|
||||
'${ChatMessageController.kChatMessageBoxPrefix}${channel.id}',
|
||||
);
|
||||
final lastMessage = channelBox.isNotEmpty
|
||||
? channelBox.values
|
||||
.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b)
|
||||
: null;
|
||||
final lastMessage =
|
||||
channelBox.isNotEmpty ? channelBox.values.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) : null;
|
||||
if (lastMessage != null) result.add(lastMessage);
|
||||
channelBox.close();
|
||||
}
|
||||
|
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())}';
|
||||
}
|
35
lib/providers/link_preview.dart
Normal file
@ -0,0 +1,35 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/link.dart';
|
||||
|
||||
class SnLinkPreviewProvider {
|
||||
late final SnNetworkProvider _sn;
|
||||
|
||||
final Map<String, SnLinkMeta> _cache = {};
|
||||
|
||||
SnLinkPreviewProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
}
|
||||
|
||||
Future<SnLinkMeta?> getLinkMeta(String url) async {
|
||||
final b64 = utf8.fuse(base64Url);
|
||||
final target = b64.encode(url);
|
||||
if (_cache.containsKey(target)) return _cache[target];
|
||||
|
||||
log('[LinkPreview] Fetching $url ($target)');
|
||||
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/re/link/$target');
|
||||
final meta = SnLinkMeta.fromJson(resp.data);
|
||||
_cache[url] = meta;
|
||||
return meta;
|
||||
} catch (err) {
|
||||
log('[LinkPreview] Failed to fetch $url ($target)...');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
|
||||
screen: 'realm',
|
||||
label: 'screenRealm',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
|
||||
screen: 'news',
|
||||
label: 'screenNews',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||
screen: 'album',
|
||||
@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
|
||||
|
||||
List<AppNavDestination> destinations = [];
|
||||
|
||||
int get pinnedDestinationCount =>
|
||||
destinations.where((ele) => ele.isPinned).length;
|
||||
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
|
||||
|
||||
NavigationProvider() {
|
||||
buildDestinations(kDefaultPinnedDestination);
|
||||
@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool isIndexInRange(int min, int max) {
|
||||
return _currentIndex != null &&
|
||||
_currentIndex! >= min &&
|
||||
_currentIndex! < max;
|
||||
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
|
||||
}
|
||||
|
||||
void autoDetectIndex(GoRouter? state) {
|
||||
if (state == null) return;
|
||||
final idx = destinations.indexWhere(
|
||||
(ele) =>
|
||||
ele.screen ==
|
||||
state.routerDelegate.currentConfiguration.last.route.name,
|
||||
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
|
||||
);
|
||||
_currentIndex = idx == -1 ? null : idx;
|
||||
notifyListeners();
|
||||
|
@ -4,26 +4,26 @@ import 'dart:io';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
|
||||
class NotificationProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserProvider _ua;
|
||||
late final WebSocketProvider _ws;
|
||||
late final ConfigProvider _cfg;
|
||||
|
||||
NotificationProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ua = context.read<UserProvider>();
|
||||
|
||||
// Delay to wait user provider ready to use
|
||||
Future.delayed(const Duration(milliseconds: 3000), () async {
|
||||
if (!_ua.isAuthorized) return;
|
||||
log("Registering push notifications...");
|
||||
await registerPushNotifications();
|
||||
log("Registered push notification subscriber successfully!");
|
||||
});
|
||||
_ws = context.read<WebSocketProvider>();
|
||||
_cfg = context.read<ConfigProvider>();
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
@ -70,4 +70,23 @@ class NotificationProvider extends ChangeNotifier {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<SnNotification> notifications = List.empty(growable: true);
|
||||
|
||||
void listen() {
|
||||
_ws.stream.stream.listen((event) {
|
||||
if (event.method == 'notifications.new') {
|
||||
final notification = SnNotification.fromJson(event.payload!);
|
||||
notifications.add(notification);
|
||||
notifyListeners();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.lightImpact();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
notifications.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,11 @@ class SnPostContentProvider {
|
||||
if (out[i].body['thumbnail'] != null) {
|
||||
rids.add(out[i].body['thumbnail']);
|
||||
}
|
||||
if (out[i].repostTo != null) {
|
||||
out[i] = out[i].copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
@ -48,6 +53,11 @@ class SnPostContentProvider {
|
||||
if (out.body['thumbnail'] != null) {
|
||||
rids.add(out.body['thumbnail']);
|
||||
}
|
||||
if (out.repostTo != null) {
|
||||
out = out.copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
||||
);
|
||||
}
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
out = out.copyWith(
|
||||
@ -73,12 +83,16 @@ class SnPostContentProvider {
|
||||
int offset = 0,
|
||||
String? type,
|
||||
String? author,
|
||||
Iterable<String>? categories,
|
||||
Iterable<String>? tags,
|
||||
}) async {
|
||||
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
if (type != null) 'type': type,
|
||||
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(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
@ -108,12 +122,14 @@ class SnPostContentProvider {
|
||||
int take = 10,
|
||||
int offset = 0,
|
||||
Iterable<String>? tags,
|
||||
Iterable<String>? categories,
|
||||
}) async {
|
||||
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
'probe': searchTerm,
|
||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
||||
});
|
||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
|
||||
class SnRelationshipProvider {
|
||||
late final SnNetworkProvider _sn;
|
||||
@ -9,6 +10,15 @@ class SnRelationshipProvider {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
}
|
||||
|
||||
Future<SnRelationship?> getRelationship(int relatedId) async {
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/id/users/me/relations/$relatedId');
|
||||
return SnRelationship.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateRelationship(
|
||||
int relatedId,
|
||||
int status,
|
||||
|
@ -21,7 +21,7 @@ class SnAttachmentProvider {
|
||||
|
||||
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
|
||||
for (final item in items) {
|
||||
if ((item.isAnalyzed && item.isUploaded) || noCheck) {
|
||||
if (item.isAnalyzed || noCheck) {
|
||||
_cache[item.rid] = item;
|
||||
}
|
||||
}
|
||||
@ -34,15 +34,14 @@ class SnAttachmentProvider {
|
||||
|
||||
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
||||
final out = SnAttachment.fromJson(resp.data);
|
||||
if (out.isAnalyzed && out.isUploaded) {
|
||||
if (out.isAnalyzed) {
|
||||
_cache[rid] = out;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<List<SnAttachment?>> getMultiple(List<String> rids,
|
||||
{noCache = false}) async {
|
||||
Future<List<SnAttachment?>> getMultiple(List<String> rids, {noCache = false}) async {
|
||||
final result = List<SnAttachment?>.filled(rids.length, null);
|
||||
final Map<String, int> randomMapping = {};
|
||||
for (int i = 0; i < rids.length; i++) {
|
||||
@ -63,13 +62,12 @@ class SnAttachmentProvider {
|
||||
'id': pendingFetch.join(','),
|
||||
},
|
||||
);
|
||||
final out = resp.data['data']
|
||||
.map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
|
||||
.toList();
|
||||
final List<SnAttachment?> out =
|
||||
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
|
||||
|
||||
for (final item in out) {
|
||||
if (item == null) continue;
|
||||
if (item.isAnalyzed && item.isUploaded) {
|
||||
if (item.isAnalyzed) {
|
||||
_cache[item.rid] = item;
|
||||
}
|
||||
result[randomMapping[item.rid]!] = item;
|
||||
@ -79,10 +77,7 @@ class SnAttachmentProvider {
|
||||
return result;
|
||||
}
|
||||
|
||||
static Map<String, String> mimetypeOverrides = {
|
||||
'mov': 'video/quicktime',
|
||||
'mp4': 'video/mp4'
|
||||
};
|
||||
static Map<String, String> mimetypeOverrides = {'mov': 'video/quicktime', 'mp4': 'video/mp4'};
|
||||
|
||||
Future<SnAttachment> directUploadOne(
|
||||
Uint8List data,
|
||||
@ -91,13 +86,11 @@ class SnAttachmentProvider {
|
||||
Map<String, dynamic>? metadata, {
|
||||
String? mimetype,
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||
final fileAlt = filename.contains('.')
|
||||
? filename.substring(0, filename.lastIndexOf('.'))
|
||||
: filename;
|
||||
final fileExt =
|
||||
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
|
||||
String? mimetypeOverride;
|
||||
if (mimetype != null) {
|
||||
@ -116,6 +109,7 @@ class SnAttachmentProvider {
|
||||
final resp = await _sn.client.post(
|
||||
'/cgi/uc/attachments',
|
||||
data: formData,
|
||||
queryParameters: {'analyzeNow': analyzeNow},
|
||||
onSendProgress: (count, total) {
|
||||
if (onProgress != null) {
|
||||
onProgress(count / total);
|
||||
@ -126,18 +120,15 @@ class SnAttachmentProvider {
|
||||
return SnAttachment.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<(SnAttachment, int)> chunkedUploadInitialize(
|
||||
Future<(SnAttachmentFragment, int)> chunkedUploadInitialize(
|
||||
int size,
|
||||
String filename,
|
||||
String pool,
|
||||
Map<String, dynamic>? metadata, {
|
||||
String? mimetype,
|
||||
}) async {
|
||||
final fileAlt = filename.contains('.')
|
||||
? filename.substring(0, filename.lastIndexOf('.'))
|
||||
: filename;
|
||||
final fileExt =
|
||||
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
|
||||
String? mimetypeOverride;
|
||||
if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
|
||||
@ -146,7 +137,7 @@ class SnAttachmentProvider {
|
||||
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,
|
||||
'name': filename,
|
||||
'pool': pool,
|
||||
@ -155,21 +146,20 @@ class SnAttachmentProvider {
|
||||
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
||||
});
|
||||
|
||||
return (
|
||||
SnAttachment.fromJson(resp.data['meta']),
|
||||
resp.data['chunk_size'] as int
|
||||
);
|
||||
return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
|
||||
}
|
||||
|
||||
Future<SnAttachment> _chunkedUploadOnePart(
|
||||
Future<dynamic> _chunkedUploadOnePart(
|
||||
Uint8List data,
|
||||
String rid,
|
||||
String cid, {
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final resp = await _sn.client.post(
|
||||
'/cgi/uc/attachments/multipart/$rid/$cid',
|
||||
'/cgi/uc/fragments/$rid/$cid',
|
||||
data: data,
|
||||
queryParameters: {'analyzeNow': analyzeNow},
|
||||
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
||||
onSendProgress: (count, total) {
|
||||
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(
|
||||
XFile file,
|
||||
SnAttachment place,
|
||||
SnAttachmentFragment place,
|
||||
int chunkSize, {
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final Map<String, dynamic> chunks = place.fileChunks ?? {};
|
||||
var currentTask = 0;
|
||||
final Map<String, dynamic> chunks = place.fileChunks;
|
||||
var completedTasks = 0;
|
||||
|
||||
final queue = Queue<Future<void>>();
|
||||
final activeTasks = <Future<void>>[];
|
||||
|
||||
late SnAttachment out;
|
||||
|
||||
for (final entry in chunks.entries) {
|
||||
queue.add(() async {
|
||||
final beginCursor = entry.value * chunkSize;
|
||||
@ -200,25 +197,28 @@ class SnAttachmentProvider {
|
||||
(entry.value + 1) * chunkSize,
|
||||
await file.length(),
|
||||
);
|
||||
final data = Uint8List.fromList(await file
|
||||
.openRead(beginCursor, endCursor)
|
||||
.expand((chunk) => chunk)
|
||||
.toList());
|
||||
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
|
||||
|
||||
place = await _chunkedUploadOnePart(
|
||||
final result = await _chunkedUploadOnePart(
|
||||
data,
|
||||
place.rid,
|
||||
entry.key,
|
||||
onProgress: (chunkProgress) {
|
||||
final overallProgress =
|
||||
(currentTask + chunkProgress) / chunks.length;
|
||||
if (onProgress != null) {
|
||||
onProgress(overallProgress);
|
||||
}
|
||||
analyzeNow: analyzeNow,
|
||||
onProgress: (progress) {
|
||||
final overallProgress = (completedTasks + progress) / chunks.length;
|
||||
onProgress?.call(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);
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,39 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
const kAtkStoreKey = 'nex_user_atk';
|
||||
const kRtkStoreKey = 'nex_user_rtk';
|
||||
|
||||
const kNetworkServerDefault = 'https://api.sn.solsynth.dev';
|
||||
const kNetworkServerStoreKey = 'app_server_url';
|
||||
|
||||
const kNetworkServerDirectory = [
|
||||
('Solar Network', 'https://api.sn.solsynth.dev'),
|
||||
('Local', 'http://localhost:8001'),
|
||||
];
|
||||
|
||||
Completer<String?>? _refreshCompleter;
|
||||
|
||||
class SnNetworkProvider {
|
||||
late final Dio client;
|
||||
|
||||
late final SharedPreferences _prefs;
|
||||
late final ConfigProvider _config;
|
||||
late final HomeWidgetProvider _home;
|
||||
|
||||
String? _userAgent;
|
||||
|
||||
SnNetworkProvider(BuildContext context) {
|
||||
_home = context.read<HomeWidgetProvider>();
|
||||
|
||||
SnNetworkProvider() {
|
||||
client = Dio();
|
||||
|
||||
client.interceptors.add(RetryInterceptor(
|
||||
@ -46,23 +56,104 @@ class SnNetworkProvider {
|
||||
if (atk != null) {
|
||||
options.headers['Authorization'] = 'Bearer $atk';
|
||||
}
|
||||
if (_userAgent != null) {
|
||||
options.headers['User-Agent'] = _userAgent!;
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
_prefs = prefs;
|
||||
client.options.baseUrl =
|
||||
_prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||
_config = context.read<ConfigProvider>();
|
||||
_config.initialize().then((_) {
|
||||
_prefs = _config.prefs;
|
||||
client.options.baseUrl = _config.serverUrl;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
if (kIsWeb) {
|
||||
final deviceInfo = await DeviceInfoPlugin().webBrowserInfo;
|
||||
platformInfo = 'Web; ${deviceInfo.vendor}';
|
||||
} else if (Platform.isAndroid) {
|
||||
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||
platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
|
||||
} else if (Platform.isIOS) {
|
||||
final deviceInfo = await DeviceInfoPlugin().iosInfo;
|
||||
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
|
||||
} else if (Platform.isMacOS) {
|
||||
final deviceInfo = await DeviceInfoPlugin().macOsInfo;
|
||||
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
|
||||
} else if (Platform.isWindows) {
|
||||
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
|
||||
platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
|
||||
} else if (Platform.isLinux) {
|
||||
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
|
||||
platformInfo = 'Linux; ${deviceInfo.prettyName}';
|
||||
} else {
|
||||
platformInfo = 'Unknown';
|
||||
}
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
return 'Solian/${packageInfo.version}+${packageInfo.buildNumber} ($platformInfo)';
|
||||
}
|
||||
|
||||
Future<void> initializeUserAgent() async {
|
||||
_userAgent = await _getUserAgent();
|
||||
}
|
||||
|
||||
final tkLock = Lock();
|
||||
|
||||
Completer<String?>? _refreshCompleter;
|
||||
|
||||
Future<String?> getFreshAtk() async {
|
||||
return await _getFreshAtk(client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), (atk, rtk) {
|
||||
setTokenPair(atk, rtk);
|
||||
});
|
||||
}
|
||||
|
||||
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async {
|
||||
if (_refreshCompleter != null) {
|
||||
return await _refreshCompleter!.future;
|
||||
} else {
|
||||
@ -70,7 +161,6 @@ class SnNetworkProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
var atk = _prefs.getString(kAtkStoreKey);
|
||||
if (atk != null) {
|
||||
final atkParts = atk.split('.');
|
||||
if (atkParts.length != 3) {
|
||||
@ -96,7 +186,13 @@ class SnNetworkProvider {
|
||||
final exp = jsonDecode(payload)['exp'];
|
||||
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
||||
log('Access token need refresh, doing it at ${DateTime.now()}');
|
||||
atk = await refreshToken();
|
||||
final result = await _refreshToken(client.options.baseUrl, rtk);
|
||||
if (result == null) {
|
||||
atk = null;
|
||||
} else {
|
||||
atk = result.$1;
|
||||
onRefresh?.call(atk, result.$2);
|
||||
}
|
||||
}
|
||||
|
||||
if (atk != null) {
|
||||
@ -134,24 +230,32 @@ class SnNetworkProvider {
|
||||
|
||||
Future<String?> refreshToken() async {
|
||||
final rtk = _prefs.getString(kRtkStoreKey);
|
||||
final result = await _refreshToken(client.options.baseUrl, rtk);
|
||||
if (result == null) return null;
|
||||
_prefs.setString(kAtkStoreKey, result.$1);
|
||||
_prefs.setString(kRtkStoreKey, result.$2);
|
||||
return result.$1;
|
||||
}
|
||||
|
||||
static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async {
|
||||
if (rtk == null) return null;
|
||||
|
||||
final dio = Dio();
|
||||
dio.options.baseUrl = client.options.baseUrl;
|
||||
dio.options.baseUrl = baseUrl;
|
||||
|
||||
final resp = await dio.post('/cgi/id/auth/token', data: {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': rtk,
|
||||
});
|
||||
|
||||
final atk = resp.data['access_token'];
|
||||
final nRtk = resp.data['refresh_token'];
|
||||
setTokenPair(atk, nRtk);
|
||||
final String atk = resp.data['access_token'];
|
||||
final String nRtk = resp.data['refresh_token'];
|
||||
|
||||
return atk;
|
||||
return (atk, nRtk);
|
||||
}
|
||||
|
||||
void setBaseUrl(String url) {
|
||||
_config.serverUrl = url;
|
||||
client.options.baseUrl = url;
|
||||
}
|
||||
}
|
||||
|
38
lib/providers/sn_sticker.dart
Normal file
@ -0,0 +1,38 @@
|
||||
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 = {};
|
||||
|
||||
SnStickerProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
}
|
||||
|
||||
bool hasNotSticker(String alias) {
|
||||
return _cache.containsKey(alias) && _cache[alias] == null;
|
||||
}
|
||||
|
||||
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);
|
||||
_cache[alias] = sticker;
|
||||
|
||||
return sticker;
|
||||
} catch (err) {
|
||||
_cache[alias] = null;
|
||||
log('[Sticker] Failed to lookup sticker $alias: $err');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
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:surface/theme.dart';
|
||||
|
||||
@ -11,8 +13,8 @@ class ThemeProvider extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
void reloadTheme({bool? useMaterial3}) {
|
||||
createAppThemeSet().then((value) {
|
||||
void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) {
|
||||
createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) {
|
||||
theme = value;
|
||||
notifyListeners();
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
|
||||
@ -11,24 +12,27 @@ class UserProvider extends ChangeNotifier {
|
||||
SnAccount? user;
|
||||
|
||||
late final SnNetworkProvider _sn;
|
||||
late final ConfigProvider _config;
|
||||
|
||||
UserProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_config = context.read<ConfigProvider>();
|
||||
}
|
||||
|
||||
Future<String?> get atk async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(kAtkStoreKey);
|
||||
}
|
||||
|
||||
UserProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
final value = prefs.getString(kAtkStoreKey);
|
||||
isAuthorized = value != null;
|
||||
notifyListeners();
|
||||
refreshUser().then((value) {
|
||||
if (value != null) {
|
||||
log('Logged in as @${value.name}');
|
||||
}
|
||||
});
|
||||
Future<void> initialize() async {
|
||||
final value = _config.prefs.getString(kAtkStoreKey);
|
||||
isAuthorized = value != null;
|
||||
notifyListeners();
|
||||
refreshUser().then((value) async {
|
||||
if (value != null) {
|
||||
log('Logged in as @${value.name}');
|
||||
log('Atk: ${await atk}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,21 +23,19 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
WebSocketProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ua = context.read<UserProvider>();
|
||||
}
|
||||
|
||||
// Wait for the userinfo provide initialize authorization status
|
||||
Future.delayed(const Duration(milliseconds: 250), () async {
|
||||
if (_ua.isAuthorized) {
|
||||
log('[WebSocket] Connecting to the server...');
|
||||
await connect();
|
||||
} else {
|
||||
log('[WebSocket] Unable connect to the server, unauthorized.');
|
||||
}
|
||||
});
|
||||
Future<void> tryConnect() async {
|
||||
if (isConnected) return;
|
||||
if (!_ua.isAuthorized) return;
|
||||
|
||||
log('[WebSocket] Connecting to the server...');
|
||||
await connect();
|
||||
}
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
if (!_ua.isAuthorized) return;
|
||||
if (isConnected) {
|
||||
if (isConnected || conn != null) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@ -79,6 +77,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
if (conn != null) {
|
||||
conn!.sink.close();
|
||||
}
|
||||
conn = null;
|
||||
isConnected = false;
|
||||
notifyListeners();
|
||||
}
|
||||
@ -98,7 +97,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
onError: (err) {
|
||||
isConnected = false;
|
||||
notifyListeners();
|
||||
Future.delayed(const Duration(seconds: 11), () => connect());
|
||||
Future.delayed(const Duration(seconds: 1), () => connect());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
60
lib/providers/widget.dart
Normal file
@ -0,0 +1,60 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:home_widget/home_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
|
||||
class HomeWidgetProvider {
|
||||
HomeWidgetProvider(BuildContext context);
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
|
||||
if (Platform.isIOS) {
|
||||
await HomeWidget.setAppGroupId("group.solsynth.solian");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveWidgetData(String id, dynamic data, {bool update = true}) async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
|
||||
await HomeWidget.saveWidgetData(id, jsonEncode(data));
|
||||
if (update) await updateWidget();
|
||||
}
|
||||
|
||||
Future<void> updateWidget() async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
|
||||
if (Platform.isIOS) {
|
||||
const widgets = ["SolarRandomPostWidget", "SolarCheckInWidget"];
|
||||
for (final widget in widgets) {
|
||||
await HomeWidget.updateWidget(
|
||||
name: widget,
|
||||
iOSName: widget,
|
||||
);
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
const widgets = ["RandomPostWidget", "CheckInWidget"];
|
||||
for (final widget in widgets) {
|
||||
await HomeWidget.updateWidget(
|
||||
androidName: "${widget}Receiver",
|
||||
qualifiedAndroidName: "dev.solsynth.solian.widgets.${widget}Receiver",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> widgetUpdateRandomPost() async {
|
||||
final snc = await SnNetworkProvider.createOffContextClient();
|
||||
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
||||
final post = SnPost.fromJson(resp.data['data'][0]);
|
||||
await HomeWidget.saveWidgetData("int_random_post", jsonEncode(post.toJson()));
|
||||
await HomeWidget.updateWidget(
|
||||
name: "SolarRandomPostWidget",
|
||||
iOSName: "SolarRandomPostWidget",
|
||||
androidName: "RandomPostWidgetReceiver",
|
||||
qualifiedAndroidName: "dev.solsynth.solian.widgets.RandomPostWidgetReceiver",
|
||||
);
|
||||
}
|
430
lib/router.dart
@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.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/publishers/publisher_edit.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/friend.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/post/post_detail.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
@ -28,291 +32,227 @@ import 'package:surface/screens/realm.dart';
|
||||
import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/settings.dart';
|
||||
import 'package:surface/screens/sharing.dart';
|
||||
import 'package:surface/screens/wallet.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/about.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.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 = [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(
|
||||
body: child,
|
||||
showAppBar: false,
|
||||
),
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: 'home',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts',
|
||||
name: 'explore',
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: 'home',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const HomeScreen(),
|
||||
path: '/write/:mode',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) => 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: '/posts',
|
||||
name: 'explore',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const ExploreScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/write/:mode',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) => AppBackground(
|
||||
isLessOptimization: true,
|
||||
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'] ?? '',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => const AppBackground(
|
||||
isLessOptimization: true,
|
||||
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(
|
||||
isLessOptimization: true,
|
||||
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(
|
||||
isLessOptimization: true,
|
||||
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(
|
||||
isLessOptimization: true,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const AlbumScreen(),
|
||||
path: '/search',
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => PostSearchScreen(
|
||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/friend',
|
||||
name: 'friend',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const FriendScreen(),
|
||||
),
|
||||
path: '/publishers/:name',
|
||||
name: 'postPublisher',
|
||||
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notification',
|
||||
name: 'notification',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const NotificationScreen(),
|
||||
path: '/:slug',
|
||||
name: 'postDetail',
|
||||
builder: (context, state) => PostDetailScreen(
|
||||
slug: state.pathParameters['slug']!,
|
||||
preload: state.extra as SnPost?,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
|
||||
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: [
|
||||
GoRoute(
|
||||
path: '/auth/login',
|
||||
name: 'authLogin',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: LoginScreen(),
|
||||
path: '/:scope/:alias',
|
||||
name: 'chatRoom',
|
||||
builder: (context, state) => ChatRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/register',
|
||||
name: 'authRegister',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: RegisterScreen(),
|
||||
path: '/:scope/:alias/call',
|
||||
name: 'chatCallRoom',
|
||||
builder: (context, state) => CallRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/reports',
|
||||
name: 'abuseReport',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AbuseReportScreen(),
|
||||
path: '/:scope/:alias/detail',
|
||||
name: 'channelDetail',
|
||||
builder: (context, state) => ChannelDetailScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: ProfileEditScreen(),
|
||||
),
|
||||
),
|
||||
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']!,
|
||||
),
|
||||
path: '/manage',
|
||||
name: 'chatManage',
|
||||
builder: (context, state) => ChatManageScreen(
|
||||
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
path: '/realm',
|
||||
name: 'realm',
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
transitionsBuilder: _fadeThroughTransition,
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: SettingsScreen(),
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
builder: (context, state) => RealmManageScreen(
|
||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AboutScreen(),
|
||||
),
|
||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/:hash',
|
||||
name: 'newsDetail',
|
||||
builder: (context, state) => NewsDetailScreen(
|
||||
hash: state.pathParameters['hash']!,
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
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(),
|
||||
),
|
||||
];
|
||||
|
||||
@ -320,7 +260,9 @@ final appRouter = GoRouter(
|
||||
routes: [
|
||||
ShellRoute(
|
||||
routes: _appRoutes,
|
||||
builder: (context, state, child) => AppRootScaffold(body: child),
|
||||
builder: (context, state, child) => AppRootScaffold(
|
||||
body: AppSharingListener(child: child),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../types/account.dart';
|
||||
|
||||
@ -39,7 +40,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
void _showAbuseReportDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _AbuseReportDialog(),
|
||||
builder: (context) => AbuseReportDialog(),
|
||||
).then((value) {
|
||||
if (value == true && mounted) {
|
||||
_fetchReports();
|
||||
@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAbuseReport').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _reports.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return ListTile(
|
||||
@ -91,19 +97,29 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AbuseReportDialog extends StatefulWidget {
|
||||
const _AbuseReportDialog({super.key});
|
||||
class AbuseReportDialog extends StatefulWidget {
|
||||
final String? resourceLocation;
|
||||
|
||||
const AbuseReportDialog({super.key, this.resourceLocation});
|
||||
|
||||
@override
|
||||
State<_AbuseReportDialog> createState() => _AbuseReportDialogState();
|
||||
State<AbuseReportDialog> createState() => _AbuseReportDialogState();
|
||||
}
|
||||
|
||||
class _AbuseReportDialogState extends State<_AbuseReportDialog> {
|
||||
class _AbuseReportDialogState extends State<AbuseReportDialog> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final _resourceController = TextEditingController();
|
||||
final _reasonController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.resourceLocation != null) {
|
||||
_resourceController.text = widget.resourceLocation!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_resourceController.dispose();
|
||||
@ -144,6 +160,7 @@ class _AbuseReportDialogState extends State<_AbuseReportDialog> {
|
||||
const Gap(12),
|
||||
TextField(
|
||||
controller: _resourceController,
|
||||
readOnly: widget.resourceLocation != null,
|
||||
maxLength: null,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@ -12,6 +14,8 @@ import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/widgets/account/account_image.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/universal_image.dart';
|
||||
|
||||
class AccountScreen extends StatelessWidget {
|
||||
const AccountScreen({super.key});
|
||||
@ -19,11 +23,39 @@ class AccountScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenAccount").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: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.settings, fill: 1),
|
||||
@ -82,16 +114,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
);
|
||||
}).padding(all: 20),
|
||||
).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(
|
||||
title: Text('accountPublishers').tr(),
|
||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||
@ -112,52 +134,55 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
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(
|
||||
title: Text('accountLogout').tr(),
|
||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.logout),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
onTap: () async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'accountLogoutConfirmTitle'.tr(),
|
||||
'accountLogoutConfirm'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if(!context.mounted) return;
|
||||
if (value) ua.logoutUser();
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
ws.disconnect();
|
||||
Hive.deleteFromDisk();
|
||||
});
|
||||
},
|
||||
),
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
);
|
||||
|
||||
if (!confirm) return;
|
||||
if (!context.mounted) return;
|
||||
ua.logoutUser();
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
ws.disconnect();
|
||||
await Hive.deleteFromDisk();
|
||||
await Hive.initFlutter();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
66
lib/screens/account/account_settings.dart
Normal file
@ -0,0 +1,66 @@
|
||||
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:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountSettingsScreen extends StatelessWidget {
|
||||
const AccountSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,354 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
const Map<String, (String, IconData, Color)> kBadgesMeta = {
|
||||
'company.staff': (
|
||||
'badgeCompanyStaff',
|
||||
Symbols.tools_wrench,
|
||||
Colors.teal,
|
||||
),
|
||||
'site.migration': (
|
||||
'badgeSiteMigration',
|
||||
Symbols.flag,
|
||||
Colors.orange,
|
||||
),
|
||||
};
|
||||
|
||||
class UserScreen extends StatefulWidget {
|
||||
final String name;
|
||||
|
||||
const UserScreen({super.key, required this.name});
|
||||
|
||||
@override
|
||||
State<UserScreen> createState() => _UserScreenState();
|
||||
}
|
||||
|
||||
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
|
||||
late final ScrollController _scrollController = ScrollController();
|
||||
|
||||
SnAccount? _account;
|
||||
|
||||
Future<void> _fetchAccount() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/${widget.name}');
|
||||
if (!mounted) return;
|
||||
_account = SnAccount.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err).then((_) {
|
||||
if (mounted) Navigator.pop(context);
|
||||
});
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
SnAccountStatusInfo? _status;
|
||||
|
||||
Future<void> _fetchStatus() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/${widget.name}/status');
|
||||
if (!mounted) return;
|
||||
_status = SnAccountStatusInfo.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
double _appBarBlur = 0.0;
|
||||
|
||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
|
||||
void _updateAppBarBlur() {
|
||||
if (_scrollController.offset > _appBarHeight) return;
|
||||
setState(() {
|
||||
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchAccount().then((_) {
|
||||
_fetchStatus();
|
||||
});
|
||||
_scrollController.addListener(_updateAppBarBlur);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_updateAppBarBlur);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const kBannerAspectRatio = 7 / 16;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageHeight = _appBarHeight + kToolbarHeight + 8;
|
||||
|
||||
const labelShadows = <Shadow>[
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
];
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: _appBarHeight,
|
||||
title: _account == null
|
||||
? Text('loading').tr()
|
||||
: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _account!.nick,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: '@${_account!.name}',
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
pinned: true,
|
||||
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,
|
||||
),
|
||||
if (_account != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: _account!.avatar,
|
||||
radius: 28,
|
||||
),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_account!.nick,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).bold(),
|
||||
Text('@${_account!.name}').fontSize(13),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(right: 8),
|
||||
const Gap(12),
|
||||
Text(_account!.description).padding(horizontal: 8),
|
||||
const Gap(4),
|
||||
Card(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
_status != null
|
||||
? _status!.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
_status!.lastSeenAt != null
|
||||
? RelativeTime(context).format(
|
||||
_status!.lastSeenAt!.toLocal(),
|
||||
)
|
||||
: 'unknown',
|
||||
]),
|
||||
).padding(left: 6).opacity(0.75),
|
||||
],
|
||||
).padding(vertical: 8, horizontal: 12),
|
||||
),
|
||||
const Gap(8),
|
||||
Wrap(
|
||||
children: _account!.badges
|
||||
.map(
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(ele.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.calendar_add_on),
|
||||
const Gap(8),
|
||||
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.cake),
|
||||
const Gap(8),
|
||||
Text('accountBirthday').tr(args: [
|
||||
_account!.profile?.birthday == null
|
||||
? 'unknown'.tr()
|
||||
: DateFormat('M/d').format(
|
||||
_account!.profile!.birthday!.toLocal(),
|
||||
)
|
||||
]),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.identity_platform),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'#${_account!.id.toString().padLeft(8, '0')}',
|
||||
style: GoogleFonts.robotoMono(),
|
||||
).opacity(0.8),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
SliverToBoxAdapter(child: const Divider()),
|
||||
const SliverGap(12),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
SizedBox(
|
||||
height: 80,
|
||||
width: double.infinity,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
for (final badge in _account?.badges ?? [])
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[badge.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
title: Text(
|
||||
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
|
||||
).tr(),
|
||||
subtitle: badge.metadata['title'] != null
|
||||
? Text(badge.metadata['title'])
|
||||
: Text(
|
||||
DateFormat('y/M/d').format(badge.createdAt),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class ProfileEditScreen extends StatefulWidget {
|
||||
@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
setState(() {
|
||||
_birthday = newDate;
|
||||
_birthdayController.text =
|
||||
DateFormat(_kDateFormat).format(_birthday!);
|
||||
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
if (image == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final ImageProvider imageProvider =
|
||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios = place == 'banner'
|
||||
? [CropAspectRatio(width: 16, height: 7)]
|
||||
: [CropAspectRatio(width: 1, height: 1)];
|
||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios =
|
||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final rawBytes =
|
||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||
|
||||
try {
|
||||
final attachment = await attach.directUploadOne(
|
||||
@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Material(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountProfileEdit').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Material(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? 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: () {
|
||||
_updateImage('avatar');
|
||||
_updateImage('banner');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
const Gap(8 + 28),
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
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(),
|
||||
),
|
||||
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: () {
|
||||
_updateImage('avatar');
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldLastName'.tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
const Gap(8 + 28),
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
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),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldBirthday'.tr(),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldBirthday'.tr(),
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding + 8),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isBusy ? null : _updateUserInfo,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('apply').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
],
|
||||
],
|
||||
).padding(horizontal: padding + 8),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isBusy ? null : _updateUserInfo,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('apply').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
712
lib/screens/account/profile_page.dart
Normal file
@ -0,0 +1,712 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/experience.dart';
|
||||
import 'package:surface/providers/relationship.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
const Map<String, (String, IconData, Color)> kBadgesMeta = {
|
||||
'company.staff': (
|
||||
'badgeCompanyStaff',
|
||||
Symbols.tools_wrench,
|
||||
Colors.teal,
|
||||
),
|
||||
'site.migration': (
|
||||
'badgeSiteMigration',
|
||||
Symbols.flag,
|
||||
Colors.orange,
|
||||
),
|
||||
};
|
||||
|
||||
class UserScreen extends StatefulWidget {
|
||||
final String name;
|
||||
|
||||
const UserScreen({super.key, required this.name});
|
||||
|
||||
@override
|
||||
State<UserScreen> createState() => _UserScreenState();
|
||||
}
|
||||
|
||||
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
|
||||
late final ScrollController _scrollController = ScrollController();
|
||||
|
||||
SnAccount? _account;
|
||||
|
||||
Future<void> _fetchAccount() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/${widget.name}');
|
||||
if (!mounted) return;
|
||||
_account = SnAccount.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err).then((_) {
|
||||
if (mounted) Navigator.pop(context);
|
||||
});
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Future<void> _fetchStatus() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/${widget.name}/status');
|
||||
if (!mounted) return;
|
||||
_status = SnAccountStatusInfo.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
List<SnPublisher>? _publishers;
|
||||
|
||||
Future<void> _fetchPublishers() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}');
|
||||
_publishers = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
SnRelationship? _accountRelationship;
|
||||
|
||||
Future<void> _addFriend() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/id/users/me/relations/friend', data: {
|
||||
'related': _account!.name,
|
||||
});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('friendRequestSent'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _blockAccount() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/id/users/me/relations/block', data: {
|
||||
'related': _account!.name,
|
||||
});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unblockAccount() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showAbuseReportDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AbuseReportDialog(
|
||||
resourceLocation: 'user:${_account?.name}',
|
||||
),
|
||||
).then((value) {
|
||||
if (value == true && mounted) {
|
||||
_fetchAccount();
|
||||
context.showSnackbar('abuseReportSubmitted'.tr());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
double _appBarBlur = 0.0;
|
||||
|
||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
|
||||
void _updateAppBarBlur() {
|
||||
if (_scrollController.offset > _appBarHeight) return;
|
||||
setState(() {
|
||||
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchAccount().then((_) async {
|
||||
if (!mounted) return;
|
||||
|
||||
_fetchStatus();
|
||||
_fetchPublishers();
|
||||
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
_accountRelationship = await rel.getRelationship(_account!.id);
|
||||
if (mounted) setState(() {});
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
_scrollController.addListener(_updateAppBarBlur);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_updateAppBarBlur);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const kBannerAspectRatio = 7 / 16;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageHeight = _appBarHeight + kToolbarHeight + 8;
|
||||
|
||||
const labelShadows = <Shadow>[
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
];
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
child: SliverAppBar(
|
||||
expandedHeight: _appBarHeight,
|
||||
title: _account == null
|
||||
? Text('loading').tr()
|
||||
: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _account!.nick,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: '@${_account!.name}',
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
pinned: true,
|
||||
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,
|
||||
),
|
||||
),
|
||||
if (_account != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: _account!.avatar,
|
||||
radius: 28,
|
||||
),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_account!.nick,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).bold(),
|
||||
Text('@${_account!.name}').fontSize(13),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
onTap: _showAbuseReportDialog,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.flag),
|
||||
const Gap(16),
|
||||
Text('report').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_accountRelationship == null)
|
||||
PopupMenuItem(
|
||||
onTap: _addFriend,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.person_add),
|
||||
const Gap(16),
|
||||
Text('friendNew').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_accountRelationship?.status != 2)
|
||||
PopupMenuItem(
|
||||
onTap: _blockAccount,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.block),
|
||||
const Gap(16),
|
||||
Text('friendBlock').tr(),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
PopupMenuItem(
|
||||
onTap: _unblockAccount,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.block),
|
||||
const Gap(16),
|
||||
Text('friendUnblock').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(right: 8),
|
||||
const Gap(12),
|
||||
Text(_account!.description).padding(horizontal: 8),
|
||||
const Gap(4),
|
||||
Card(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
_status != null
|
||||
? _status!.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
_status!.lastSeenAt != null
|
||||
? RelativeTime(context).format(
|
||||
_status!.lastSeenAt!.toLocal(),
|
||||
)
|
||||
: 'unknown',
|
||||
]),
|
||||
).padding(left: 6).opacity(0.75),
|
||||
],
|
||||
).padding(vertical: 8, horizontal: 12),
|
||||
),
|
||||
const Gap(8),
|
||||
Wrap(
|
||||
children: _account!.badges
|
||||
.map(
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(ele.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.calendar_add_on),
|
||||
const Gap(8),
|
||||
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.cake),
|
||||
const Gap(8),
|
||||
Text('accountBirthday').tr(args: [
|
||||
_account!.profile?.birthday == null
|
||||
? 'unknown'.tr()
|
||||
: DateFormat('M/d').format(
|
||||
_account!.profile!.birthday!.toLocal(),
|
||||
)
|
||||
]),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.identity_platform),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'#${_account!.id.toString().padLeft(8, '0')}',
|
||||
style: GoogleFonts.robotoMono(),
|
||||
).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(all: 16),
|
||||
),
|
||||
SliverToBoxAdapter(child: const Divider()),
|
||||
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(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
SizedBox(
|
||||
height: 80,
|
||||
width: double.infinity,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
for (final badge in _account?.badges ?? [])
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[badge.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
title: Text(
|
||||
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
|
||||
).tr(),
|
||||
subtitle: badge.metadata['title'] != null
|
||||
? Text(badge.metadata['title'])
|
||||
: Text(
|
||||
DateFormat('y/M/d').format(badge.createdAt),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(child: const Divider()),
|
||||
SliverList.builder(
|
||||
itemCount: _publishers?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _publishers![idx];
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: AccountImage(
|
||||
content: ele.avatar,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||
),
|
||||
title: Text(ele.nick),
|
||||
subtitle: Text('@${ele.name}'),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).goNamed(
|
||||
'postPublisher',
|
||||
pathParameters: {'name': ele.name},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountPublisherEditScreen extends StatefulWidget {
|
||||
final String name;
|
||||
|
||||
const AccountPublisherEditScreen({super.key, required this.name});
|
||||
|
||||
@override
|
||||
State<AccountPublisherEditScreen> createState() =>
|
||||
_AccountPublisherEditScreenState();
|
||||
State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState();
|
||||
}
|
||||
|
||||
class _AccountPublisherEditScreenState
|
||||
extends State<AccountPublisherEditScreen> {
|
||||
class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
SnPublisher? _publisher;
|
||||
@ -54,7 +54,7 @@ class _AccountPublisherEditScreenState
|
||||
_publisher = SnPublisher.fromJson(resp.data);
|
||||
_syncWidget();
|
||||
} catch (err) {
|
||||
context.showErrorDialog(err);
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
@ -75,9 +75,9 @@ class _AccountPublisherEditScreenState
|
||||
'name': _nameController.text,
|
||||
'description': _descriptionController.text,
|
||||
});
|
||||
Navigator.pop(context, true);
|
||||
if (mounted) Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
context.showErrorDialog(err);
|
||||
if(mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
@ -108,11 +108,9 @@ class _AccountPublisherEditScreenState
|
||||
if (image == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final ImageProvider imageProvider =
|
||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios = place == 'banner'
|
||||
? [CropAspectRatio(width: 16, height: 7)]
|
||||
: [CropAspectRatio(width: 1, height: 1)];
|
||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios =
|
||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -134,10 +132,7 @@ class _AccountPublisherEditScreenState
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final rawBytes =
|
||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||
|
||||
try {
|
||||
final attachment = await attach.directUploadOne(
|
||||
@ -182,7 +177,7 @@ class _AccountPublisherEditScreenState
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -199,9 +194,7 @@ class _AccountPublisherEditScreenState
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
@ -240,8 +233,7 @@ class _AccountPublisherEditScreenState
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -249,8 +241,7 @@ class _AccountPublisherEditScreenState
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldNickname'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -260,8 +251,7 @@ class _AccountPublisherEditScreenState
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldDescription'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
|
@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountPublisherNewScreen extends StatefulWidget {
|
||||
const AccountPublisherNewScreen({super.key});
|
||||
@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublisherNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -201,7 +206,7 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> {
|
||||
}
|
||||
|
||||
class _PublisherNewOrganization extends StatefulWidget {
|
||||
const _PublisherNewOrganization({super.key});
|
||||
const _PublisherNewOrganization();
|
||||
|
||||
@override
|
||||
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/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class PublisherScreen extends StatefulWidget {
|
||||
const PublisherScreen({super.key});
|
||||
@ -31,9 +32,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final resp = await sn.client.get('/cgi/co/publishers');
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
final resp = await sn.client.get('/cgi/co/publishers/me');
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublishers').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add_circle),
|
||||
onTap: () {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||
if (value == true) {
|
||||
_publishers.clear();
|
||||
_fetchPublishers();
|
||||
@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
const Divider(height: 1),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_publishers.clear();
|
||||
return _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();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_publishers.clear();
|
||||
return _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_item.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AlbumScreen extends StatefulWidget {
|
||||
@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
|
@ -7,17 +7,14 @@ 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/screens/account/factor_settings.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.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 {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@ -35,67 +32,73 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: SingleChildScrollView(
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 380),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => _LoginPickerScreen(
|
||||
key: const ValueKey(1),
|
||||
ticket: _currentTicket,
|
||||
factors: _factors,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onPickFactor: (p0) => setState(() {
|
||||
_factorPicked = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
2 => _LoginCheckScreen(
|
||||
key: const ValueKey(2),
|
||||
ticket: _currentTicket,
|
||||
factor: _factorPicked,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period = 1;
|
||||
}),
|
||||
),
|
||||
_ => _LoginLookupScreen(
|
||||
key: const ValueKey(0),
|
||||
ticket: _currentTicket,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onFactor: (p0) => setState(() {
|
||||
_factors = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
},
|
||||
).padding(all: 24),
|
||||
).center(),
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthLogin').tr(),
|
||||
),
|
||||
body: Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: SingleChildScrollView(
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 380),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => _LoginPickerScreen(
|
||||
key: const ValueKey(1),
|
||||
ticket: _currentTicket,
|
||||
factors: _factors,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onPickFactor: (p0) => setState(() {
|
||||
_factorPicked = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
2 => _LoginCheckScreen(
|
||||
key: const ValueKey(2),
|
||||
ticket: _currentTicket,
|
||||
factor: _factorPicked,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period = 1;
|
||||
}),
|
||||
),
|
||||
_ => _LoginLookupScreen(
|
||||
key: const ValueKey(0),
|
||||
ticket: _currentTicket,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onFactor: (p0) => setState(() {
|
||||
_factors = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
},
|
||||
).padding(all: 24),
|
||||
).center(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -105,6 +108,7 @@ class _LoginCheckScreen extends StatefulWidget {
|
||||
final SnAuthFactor? factor;
|
||||
final Function(SnAuthTicket?) onTicket;
|
||||
final Function onNext;
|
||||
|
||||
const _LoginCheckScreen({
|
||||
super.key,
|
||||
required this.ticket,
|
||||
@ -204,7 +208,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true)
|
||||
widget.factor!.type == 0
|
||||
? AutofillHints.password
|
||||
: AutofillHints.oneTimeCode
|
||||
],
|
||||
@ -243,6 +247,7 @@ class _LoginPickerScreen extends StatefulWidget {
|
||||
final Function(SnAuthTicket?) onTicket;
|
||||
final Function(SnAuthFactor) onPickFactor;
|
||||
final Function onNext;
|
||||
|
||||
const _LoginPickerScreen({
|
||||
super.key,
|
||||
required this.ticket,
|
||||
@ -322,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
),
|
||||
),
|
||||
secondary: Icon(
|
||||
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
|
||||
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||
),
|
||||
title: Text(
|
||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
||||
),
|
||||
kFactorTypes[x.type]?.$1 ?? 'unknown',
|
||||
).tr(),
|
||||
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||
value: _factorPicked == x.id,
|
||||
onChanged: (value) {
|
||||
@ -373,6 +378,7 @@ class _LoginLookupScreen extends StatefulWidget {
|
||||
final Function(SnAuthTicket?) onTicket;
|
||||
final Function(List<SnAuthFactor>?) onFactor;
|
||||
final Function onNext;
|
||||
|
||||
const _LoginLookupScreen({
|
||||
super.key,
|
||||
required this.ticket,
|
||||
@ -406,9 +412,11 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||
'user_id': lookupResp.data['id'],
|
||||
});
|
||||
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
if (mounted) {
|
||||
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
context.showErrorDialog(err);
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
@ -443,7 +451,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
|
||||
widget.onNext();
|
||||
} catch (err) {
|
||||
context.showErrorDialog(err);
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
|
@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StyledWidget(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
child: const Icon(
|
||||
Symbols.person_add,
|
||||
size: 28,
|
||||
),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
Text(
|
||||
'screenAuthRegister',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthRegister').tr(),
|
||||
),
|
||||
body: StyledWidget(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
child: const Icon(
|
||||
Symbols.person_add,
|
||||
size: 28,
|
||||
),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
).tr().padding(left: 4, bottom: 16),
|
||||
Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||
return 'fieldUsernameAlphanumOnly'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _usernameController,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
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()),
|
||||
),
|
||||
Text(
|
||||
'screenAuthRegister',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
).tr().padding(left: 4, bottom: 16),
|
||||
Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||
return 'fieldUsernameAlphanumOnly'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _usernameController,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('termAcceptLink'.tr()),
|
||||
const Gap(4),
|
||||
const Icon(Symbols.launch, size: 14),
|
||||
],
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
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(
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|