Compare commits

...

73 Commits

Author SHA1 Message Date
6427ec1f82 🚀 Launch 2.1.1+36 2024-12-21 23:39:04 +08:00
35dc7f4392 💄 Optimize android widget color 2024-12-21 23:36:34 +08:00
b50191970e 🚀 Launch 2.1.1+35 2024-12-21 23:30:59 +08:00
1b69e6dd42 📝 Remove todo 2024-12-21 23:26:58 +08:00
39fb4d474f App updates & web deeplink 2024-12-21 23:26:42 +08:00
392aebcad7 🐛 Fix android widgets 2024-12-21 22:55:35 +08:00
e9e3a4c474 🐛 Bug fixes on iOS widget 2024-12-21 22:38:22 +08:00
7182336a0d iOS quick reply (finished) 2024-12-21 22:19:27 +08:00
be98fe133d iOS quick response (w.i.p) 2024-12-21 21:06:14 +08:00
e458943f56 ♻️ Refactor Sn Network Provider to use without context 2024-12-21 17:23:46 +08:00
eb125fc436 Replyable message notification (w.i.p) 2024-12-21 17:15:14 +08:00
dc78f39969 Add attachment onto iOS attachment 2024-12-21 16:56:55 +08:00
f5c06bc89c 🐛 Bug fixes on iOS native image 2024-12-21 16:10:53 +08:00
d6d60e60a9 🐛 Fix ios widget image 2024-12-21 13:21:44 +08:00
435b730f3b ♻️ Android use background info too 2024-12-21 13:03:07 +08:00
73468c5c6d iOS background widget fetching 2024-12-21 11:56:18 +08:00
8db6513eef Show random post instead of featured post 2024-12-21 04:12:52 +08:00
65a8f1e6c3 🐛 Bug fixes on config refactor 2024-12-21 01:58:49 +08:00
2671ffad4b Allow setting image preview quality 2024-12-21 01:47:52 +08:00
8a628823e0 ♻️ Optimize the image
🐛 Fix image render on the web
2024-12-20 00:15:12 +08:00
94d19a1524 ⬆️ Upgrade deps to fix web build failed 2024-12-16 21:41:41 +08:00
d98f6c8d18 🐛 Bug fixes in call 2024-12-16 19:57:00 +08:00
6d0f62016a 🚀 Launch 2.1.1+31 2024-12-15 23:26:13 +08:00
7e0faba5db 🐛 Fix splash screen 2024-12-15 22:54:00 +08:00
7508a54907 🐛 Fix android widget don't work fine in release mode 2024-12-15 21:27:49 +08:00
2eb1f4b52b Android widget finishing up 2024-12-15 19:09:28 +08:00
00678c0ac8 Android featured post widget 2024-12-15 18:57:54 +08:00
abc21f858b Android check in widget 2024-12-15 18:23:12 +08:00
d67e33a41d Add basic android widget deps 2024-12-15 17:11:19 +08:00
4daff41b3e 🐛 Bug fixes with iOS related extensions 2024-12-15 16:59:41 +08:00
f92418ea4b System Share on Android 2024-12-15 13:34:32 +08:00
89c912a35b System Share on iOS 2024-12-15 12:59:18 +08:00
09ad917e5d Add share intent 2024-12-15 12:10:45 +08:00
5c377dc0b6 🐛 Fix forgot to un-comment check in widget 2024-12-15 01:17:32 +08:00
8bdaf05223 Post recommendation widget 2024-12-15 00:52:42 +08:00
e920bd954c 🐛 Fix widget size 2024-12-14 19:25:04 +08:00
e395ac87c5 Check in iOS small widget 2024-12-14 19:23:42 +08:00
026a4dfb27 Initial iOS widget target 2024-12-14 18:18:13 +08:00
df18370bde 🐛 Fix wrong line height 2024-12-14 15:24:11 +08:00
80a66136ce Link preview in posts
🐛 Fix link preview icon bugged when site icon is svg
2024-12-14 15:21:34 +08:00
1f8d47f6c3 Link preview 2024-12-14 14:46:11 +08:00
b750cc3c67 🐛 Fix cached un-analyzed attachment meta 2024-12-14 01:45:36 +08:00
b618fcc6da 🐛 Bug fixes on somewhere 2024-12-14 01:36:23 +08:00
f763c7515a ♻️ Add splash screen for loading data 2024-12-14 01:32:13 +08:00
c7d5cb48ac 🐛 Bug fixes on creating call 2024-12-14 00:14:23 +08:00
39470d7dbf ⬆️ Upgrade flutter and deps 2024-12-13 23:49:47 +08:00
4328de21ef 🐛 Bug fixes on locales and qrcode stuff 2024-12-13 18:52:39 +08:00
a3a0e8c7a2 🐛 Bug fixes
🐛 Fix cannot scan QR code from share post via image
2024-12-13 18:49:46 +08:00
210c73a831 🚀 Launch 2.0.1+23 (for iOS) 2024-12-13 00:40:12 +08:00
edaeae386e 🚀 Launch 2.0.0+23 2024-12-13 00:32:23 +08:00
be66ea354e 🌐 Update traditional chinese transitions 2024-12-13 00:05:47 +08:00
d7c1ffe3cc 💄 Optimization of sharing post via image 2024-12-13 00:05:18 +08:00
240ad7dc7e Share the post via image 2024-12-12 23:51:27 +08:00
bb5fe9c380 💄 Optimize font color on app bar in some pages 2024-12-12 22:33:10 +08:00
1347aacbc5 🚀 Launch 2.0.0+22 (for macOS)
 Remove sentry
2024-12-12 20:59:59 +08:00
8880647360 🚀 Launch 2.0.0+21 2024-12-12 01:02:58 +08:00
717bccbf3f Preload post quoted content 2024-12-12 00:48:03 +08:00
018441ea0b Support actions on user profile 2024-12-12 00:37:07 +08:00
336bb88ca4 🐛 Bug fixes due to api endpoint changed 2024-12-12 00:08:48 +08:00
811fc40d79 Allow user blocking publisher's user and report it 2024-12-11 23:53:03 +08:00
e05209ba3c Able to see realm affiliated publishers on realm view 2024-12-11 23:23:11 +08:00
623095473e 🐛 Fix logout will break something 2024-12-11 22:49:55 +08:00
f47f1b175a 👽 Update publisher api 2024-12-11 22:31:18 +08:00
3b1d291037 🐛 Bug fixes 2024-12-11 22:28:16 +08:00
2abc9808e2 🐛 Fix bad Info.plist 2024-12-11 00:39:35 +08:00
41dd7d0b64 ♻️ Refactored iOS NES
🐛 Bug fixes of NES
2024-12-11 00:31:37 +08:00
20f4e780bc 🌐 Add traditional translation for HK and TW 2024-12-10 22:47:45 +08:00
da43c940f2 🐛 Remove remove debug banner 2024-12-10 22:19:40 +08:00
a9ca8d36bc 🚀 Launch 2.0.0+20
🐛 Bug fixes upload chat attachment to wrong pool
📝 Update desktop title bar text
2024-12-10 21:45:27 +08:00
1980843ac0 📱 Optimize dashboard for large screen 2024-12-10 21:08:32 +08:00
96f6752bbe :iphone Fix responsive issue on large screen of home page 2024-12-10 00:21:32 +08:00
04b9427cdf Recommendation posts 2024-12-10 00:18:39 +08:00
eab939928f Notification card
🐛 Fix post item truncate hint from overflowing
2024-12-09 23:45:10 +08:00
105 changed files with 6179 additions and 824 deletions

3
.fvmrc
View File

@ -1,3 +0,0 @@
{
"flutter": "stable"
}

View File

@ -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.

View File

@ -9,14 +9,32 @@ plugins {
id "dev.flutter.flutter-gradle-plugin"
}
dependencies {
implementation "androidx.glance:glance:1.1.1"
implementation "androidx.glance:glance-appwidget:1.1.1"
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'io.coil-kt.coil3:coil-compose:3.0.4'
implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.4'
}
android {
buildFeatures {
compose true
}
namespace = "dev.solsynth.solian"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
}
kotlinOptions {
@ -24,21 +42,25 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "dev.solsynth.solian"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
debug {
debuggable true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

View File

@ -11,6 +11,7 @@
<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"
@ -20,12 +21,44 @@
<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 +77,30 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Widgets -->
<receiver android:name=".widgets.CheckInWidgetReceiver"
android:label="Check In"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/check_in_widget" />
</receiver>
<receiver android:name=".widgets.RandomPostWidgetReceiver"
android:label="Random Post"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/random_post_widget" />
</receiver>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

View File

@ -0,0 +1,6 @@
package dev.solsynth.solian.data
import androidx.annotation.Keep
@Keep
data class SolarPagination<T>(val count: Int, val data: List<T>)

View File

@ -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
)

View File

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

View File

@ -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
)

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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>;
}

View File

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

View File

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

View File

@ -128,6 +128,7 @@
"one": "{} social point",
"other": "{} social points"
},
"publisherAffiliatedBy": "Affiliated by {}",
"publisherRunBy": "Run by {}",
"fieldPublisherBelongToRealm": "Belongs to",
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
@ -189,6 +190,13 @@
"settingsNetworkServerPreset": "Present HyperNet Server",
"settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.",
"settingsNetworkServerSaved": "Server address saved.",
"settingsPerformance": "Performance",
"settingsImageQuality": "Image Quality",
"settingsImageQualityDescription": "Set the image quality, it will affect the decoding speed of the image.",
"settingsImageQualityLowest": "Lowest",
"settingsImageQualityLow": "Low",
"settingsImageQualityMedium": "Medium",
"settingsImageQualityHigh": "High",
"settingsMisc": "Misc",
"settingsMiscAbout": "About",
"settingsMiscAboutDescription": "View the version information of Solian.",
@ -274,6 +282,12 @@
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
"attachmentSetThumbnail": "Set thumbnail",
"attachmentUpload": "Upload",
"notification": "Notification",
"notificationUnreadCount": {
"zero": "All notifications read",
"one": "{} unread notification",
"other": "{} unread notifications"
},
"notificationUnread": "Unread",
"notificationRead": "Read",
"notificationMarkAllRead": "Mark all notifications as read",
@ -356,6 +370,8 @@
"dailyCheckNegativeHint6": "Going out",
"dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain",
"happyBirthday": "Happy birthday, {}!",
"celebrateMerryXmas": "Merry christmas, {}",
"celebrateNewYear": "Happy new year, {}",
"friendNew": "Add Friend",
"friendRequests": "Friend Requests",
"friendRequestsDescription": {
@ -389,16 +405,18 @@
"accountJoinedAt": "Joined at {}",
"accountBirthday": "Born on {}",
"accountBadge": "Badge",
"badgeCompanyStaff": "Solsynth LLC Staff",
"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",
"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.",
@ -424,5 +442,22 @@
"unauthorizedDescription": "Login to explore the entire Solar Network.",
"serviceStatus": "Service Status",
"termRelated": "Related Terms",
"appDetails": "App Details"
"appDetails": "App Details",
"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 stand by...",
"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": "正在更新,请稍后..."
}

View File

@ -35,7 +35,7 @@
"errorRequestForbidden": "被禁止的请求,您没有足够的权限去做那件事。",
"errorRequestNotFound": "您正查找的资源无法被找到。",
"errorRequestConnection": "网络连接错误,请检查您的网络状态或者检查我们的服务状态。",
"errorRequestUnknown": "位置请求错误,您可能想将此对话框截图并发送给我们。",
"errorRequestUnknown": "未知请求错误,您可能想将此对话框截图并发送给我们。",
"unknown": "未知",
"loading": "加载中…",
"prev": "上一步",
@ -112,6 +112,7 @@
"one": "{} 点社会信用点",
"other": "{} 点社会信用点"
},
"publisherAffiliatedBy": "隶属于 {}",
"publisherRunBy": "由 {} 管理",
"fieldPublisherBelongToRealm": "所属领域",
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
@ -187,6 +188,13 @@
"settingsNetworkServerPreset": "预设的 HyperNet 服务器",
"settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。",
"settingsNetworkServerSaved": "服务器地址已保存。",
"settingsPerformance": "性能",
"settingsImageQuality": "图片预览质量",
"settingsImageQualityDescription": "设置图片预览质量,会影响图片解码速度。",
"settingsImageQualityLowest": "极低",
"settingsImageQualityLow": "低",
"settingsImageQualityMedium": "中",
"settingsImageQualityHigh": "高",
"settingsMisc": "杂项",
"settingsMiscAbout": "关于",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
@ -272,6 +280,12 @@
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
"attachmentSetThumbnail": "设置缩略图",
"attachmentUpload": "上传",
"notification": "通知",
"notificationUnreadCount": {
"zero": "无未读通知",
"one": "有 {} 个未读通知",
"other": "有 {} 个未读通知"
},
"notificationUnread": "未读",
"notificationRead": "已读",
"notificationMarkAllRead": "已读所有通知",
@ -354,6 +368,8 @@
"dailyCheckNegativeHint6": "出门",
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
"happyBirthday": "生日快乐,{}",
"celebrateMerryXmas": "圣诞快乐,{}",
"celebrateNewYear": "新年快乐,{}",
"friendNew": "添加好友",
"friendRequests": "好友请求",
"friendRequestsDescription": {
@ -394,9 +410,11 @@
"accountStatusOffline": "离线",
"accountStatusLastSeen": "最后一次在 {} 上线",
"postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事",
"articleWrittenAt": "发表于 {}",
"articleEditedAt": "编辑于 {}",
"attachmentSaved": "已保存到相册",
"attachmentSavedDesktop": "已保存到下载目录",
"openInAlbum": "在相册中打开",
"postAbuseReport": "检举帖子",
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
@ -422,5 +440,22 @@
"unauthorizedDescription": "登陆以探索整个 Solar Network。",
"serviceStatus": "服务状态",
"termRelated": "相关条款",
"appDetails": "应用程序详情"
"appDetails": "应用程序详情",
"postRecommendation": "推荐帖子",
"publisherBlockHint": "屏蔽 {}",
"publisherBlockHintDescription": "你正要屏蔽此发布者的运营者,该操作也将屏蔽由同一用户运营的发布者。",
"userUnblocked": "已解除屏蔽用户 {}",
"userBlocked": "已屏蔽用户 {}",
"postSharingViaPicture": "正在生成帖子截图,请稍等片刻……",
"postImageShareReadMore": "扫描右侧 QRCode 查看全文",
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
"postShare": "分享",
"postShareImage": "分享帖图",
"appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持",
"shareIntent": "分享",
"shareIntentDescription": "您想对您分享的内容做些什么?",
"shareIntentPostStory": "发布动态",
"updateAvailable": "检测到更新可用",
"updateOngoing": "正在更新,请稍后……"
}

View File

@ -0,0 +1,457 @@
{
"screen": "頁面",
"screenAbout": "關於",
"screenHome": "首頁",
"screenExplore": "探索",
"screenAccount": "您",
"screenAuthLogin": "登陸",
"screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
"screenAuthLoginGreeting": "歡迎回來",
"screenAuthRegister": "創建賬號",
"screenAuthRegisterSubtitle": "創建一個 Solarpass 賬號",
"screenAccountPublishers": "發佈者",
"screenAccountPublisherNew": "新建發佈者",
"screenAccountPublisherEdit": "編輯發佈者",
"screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉",
"screenSettings": "設置",
"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": "登錄為 {}",
"authFactorPassword": "密碼",
"authFactorEmail": "電郵一次性驗證碼",
"accountIntroTitle": "喜歡您來!",
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
"accountLogout": "退出登錄",
"accountLogoutSubtitle": "註銷當前賬户的登陸狀態。",
"accountLogoutConfirmTitle": "您確定要退出登錄嗎?",
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
"accountPublishers": "你的發佈者",
"accountPublishersSubtitle": "管理你的公共形象。",
"accountProfileEdit": "編輯資料",
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
"accountProfileEditApplied": "個人資料修改已被應用。",
"publishersNew": "新發布者",
"publisherNewSubtitle": "創建一個新的公共身份。",
"publisherSyncWithAccount": "同步賬户信息",
"publisherTotalUpvote": "總頂數",
"publisherTotalDownvote": "總踩數",
"publisherSocialPoint": "社會信用點",
"publisherJoinedAt": "加入於 {}",
"publisherSocialPointTotal": {
"zero": "無社會信用點",
"one": "{} 點社會信用點",
"other": "{} 點社會信用點"
},
"publisherAffiliatedBy": "隸屬於 {}",
"publisherRunBy": "由 {} 管理",
"fieldPublisherBelongToRealm": "所屬領域",
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
"writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章",
"fieldPostPublisher": "帖子發佈者",
"fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題",
"fieldPostDescription": "描述",
"fieldPostTags": "標籤",
"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": "外觀",
"settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
"settingsBackgroundImageClear": "清除現存背景圖",
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
"settingsThemeMaterial3": "使用 Material You 設計範式",
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"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": "已與服務器斷開連接",
"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": "{} 個附件"
},
"addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻",
"attachmentPastedImage": "粘貼的圖片",
"attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentSetThumbnail": "設置縮略圖",
"attachmentUpload": "上傳",
"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": "忘帶傘遇上大雨",
"happyBirthday": "生日快樂,{}",
"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": "徽章",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態",
"accountStatusOnline": "在線",
"accountStatusOffline": "離線",
"accountStatusLastSeen": "最後一次在 {} 上線",
"postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事",
"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": "發佈動態"
}

View File

@ -0,0 +1,457 @@
{
"screen": "頁面",
"screenAbout": "關於",
"screenHome": "首頁",
"screenExplore": "探索",
"screenAccount": "您",
"screenAuthLogin": "登陸",
"screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
"screenAuthLoginGreeting": "歡迎回來",
"screenAuthRegister": "建立賬號",
"screenAuthRegisterSubtitle": "建立一個 Solarpass 賬號",
"screenAccountPublishers": "釋出者",
"screenAccountPublisherNew": "新建釋出者",
"screenAccountPublisherEdit": "編輯釋出者",
"screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉",
"screenSettings": "設定",
"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": "登入為 {}",
"authFactorPassword": "密碼",
"authFactorEmail": "電郵一次性驗證碼",
"accountIntroTitle": "喜歡您來!",
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
"accountLogout": "退出登入",
"accountLogoutSubtitle": "登出當前賬戶的登陸狀態。",
"accountLogoutConfirmTitle": "您確定要退出登入嗎?",
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
"accountPublishers": "你的釋出者",
"accountPublishersSubtitle": "管理你的公共形象。",
"accountProfileEdit": "編輯資料",
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
"accountProfileEditApplied": "個人資料修改已被應用。",
"publishersNew": "新發布者",
"publisherNewSubtitle": "建立一個新的公共身份。",
"publisherSyncWithAccount": "同步賬戶資訊",
"publisherTotalUpvote": "總頂數",
"publisherTotalDownvote": "總踩數",
"publisherSocialPoint": "社會信用點",
"publisherJoinedAt": "加入於 {}",
"publisherSocialPointTotal": {
"zero": "無社會信用點",
"one": "{} 點社會信用點",
"other": "{} 點社會信用點"
},
"publisherAffiliatedBy": "隸屬於 {}",
"publisherRunBy": "由 {} 管理",
"fieldPublisherBelongToRealm": "所屬領域",
"fieldPublisherBelongToRealmUnset": "未設定釋出者所屬領域",
"writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章",
"fieldPostPublisher": "帖子釋出者",
"fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題",
"fieldPostDescription": "描述",
"fieldPostTags": "標籤",
"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": "外觀",
"settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageDescription": "設定應用全域性生效的的背景圖片。",
"settingsBackgroundImageClear": "清除現存背景圖",
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
"settingsThemeMaterial3": "使用 Material You 設計正規化",
"settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。",
"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": "已與伺服器斷開連線",
"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": "{} 個附件"
},
"addAttachmentFromAlbum": "從相簿中新增附件",
"addAttachmentFromClipboard": "貼上附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝影片",
"attachmentPastedImage": "貼上的圖片",
"attachmentInsertLink": "插入連線",
"attachmentSetAsPostThumbnail": "設定為帖子縮圖",
"attachmentUnsetAsPostThumbnail": "取消設定為帖子縮圖",
"attachmentSetThumbnail": "設定縮圖",
"attachmentUpload": "上傳",
"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": "忘帶傘遇上大雨",
"happyBirthday": "生日快樂,{}",
"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": "徽章",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態",
"accountStatusOnline": "線上",
"accountStatusOffline": "離線",
"accountStatusLastSeen": "最後一次在 {} 上線",
"postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事",
"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": "釋出動態"
}

View File

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

View File

@ -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,6 +41,8 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/Analytics (11.4.0):
- Firebase/Core
- Firebase/Core (11.4.0):
@ -56,7 +57,7 @@ PODS:
- Firebase/Analytics (= 11.4.0)
- firebase_core
- Flutter
- firebase_core (3.8.1):
- firebase_core (3.9.0):
- Firebase/CoreOnly (= 11.4.0)
- Flutter
- firebase_messaging (15.1.6):
@ -85,7 +86,7 @@ PODS:
- FirebaseCoreInternal (~> 11.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)
@ -102,6 +103,8 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_udid (0.0.1):
@ -163,10 +166,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.2):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.06)
- media_kit_libs_ios_video (1.0.4):
- Flutter
@ -189,17 +198,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):
@ -216,22 +222,29 @@ PODS:
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.06)
- workmanager (0.0.1):
- Flutter
DEPENDENCIES:
- Alamofire
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- 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_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 +253,19 @@ 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`)
- 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 +277,11 @@ SPEC REPOS:
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- Kingfisher
- nanopb
- PromisesObjC
- SAMKeychain
- SDWebImage
- Sentry
- SwiftyGif
- WebRTC-SDK
@ -275,12 +290,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 +304,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_udid:
@ -297,8 +314,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 +336,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:
@ -331,34 +352,41 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
workmanager:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
firebase_core: 418aed674e9a0b8b6088aec16cde82a811f6261f
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: dbb906ef427fe96dde5854471c3dda0a50cc15f9
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
@ -368,20 +396,20 @@ SPEC CHECKSUMS:
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
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: d2bdaa1cc7915e14cf47235c34a21fcb07b00390
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
COCOAPODS: 1.16.2

View File

@ -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 = (

View File

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

View 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 {}

View 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
}

View 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
}

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>group.solsynth.solian</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@ -27,6 +29,17 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
@ -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>

View 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()
}
}

View File

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

View 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)"
}

View File

@ -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,59 @@ 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)
KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, completionHandler: { result in
var image: Data?
switch result {
case .success(let value):
image = value.image.pngData()
case .failure(let error):
print("Unable to get avatar url: \(error)")
}
let handle = INPersonHandle(value: "\(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 +119,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 {

View 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>

View 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
View 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>

View 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
}
}

View 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>

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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)
)
}

View 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>

View 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
)
}

View 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()
}
}

View 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>

View File

@ -1,21 +1,29 @@
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';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/link_preview.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/notification.dart';
import 'package:surface/providers/post.dart';
@ -26,10 +34,33 @@ 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:surface/widgets/version_label.dart';
import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart';
@pragma('vm:entry-point')
void appBackgroundDispatcher() {
Workmanager().executeTask((task, inputData) async {
log("[WorkManager] Native called background task: $task");
switch (task) {
case Workmanager.iOSBackgroundTask:
await Future.wait([widgetUpdateRandomPost()]);
return true;
case "WidgetUpdateRandomPost":
await widgetUpdateRandomPost();
return true;
default:
return true;
}
});
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -57,15 +88,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 +115,41 @@ 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)),
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)),
],
child: AppMainContent(),
child: _AppDelegate(),
),
),
breakpoints: [
@ -111,8 +161,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 +184,146 @@ 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> {
bool _isReady = false;
void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? '');
if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) {
await inAppReview.requestReview();
prefs.setBool('rating_requested', true);
} else {
log('Unable request app review, unavailable');
}
}
} else {
prefs.setString('first_boot_time', DateTime.now().toIso8601String());
}
}
Future<void> _checkForUpdate() async {
if (kIsWeb) return;
try {
final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await Dio(
BaseOptions(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
),
).get(
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
);
final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
final config = context.read<ConfigProvider>();
config.setUpdate(remoteVersionString);
log("[Update] Update available: $remoteVersionString");
}
} catch (e) {
if (mounted) context.showErrorDialog('Unable to check update: $e');
}
}
Future<void> _initialize() async {
try {
final home = context.read<HomeWidgetProvider>();
await home.initialize();
if (!mounted) return;
// The Network initialization must be done after the HomeWidget initialization
// The Network initialization will save the server url to the HomeWidget
// The Network initialization will also save initialize the Config, so it not need to be initialized again
final sn = context.read<SnNetworkProvider>();
await sn.initializeUserAgent();
if (!mounted) return;
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>();
await notify.registerPushNotifications();
} catch (err) {
if (!mounted) return;
await context.showErrorDialog(err);
} finally {
setState(() => _isReady = true);
}
}
Future<void> _postInitialization() async {
await widgetUpdateRandomPost();
}
@override
void initState() {
super.initState();
_initialize().then((_) {
_postInitialization();
_tryRequestRating();
_checkForUpdate();
});
}
@override
Widget build(BuildContext context) {
if (!_isReady) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Container(
constraints: const BoxConstraints(maxWidth: 180),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (MediaQuery.of(context).platformBrightness == Brightness.dark)
Image.asset("assets/icon/icon-dark.png", width: 64, height: 64)
else
Image.asset("assets/icon/icon.png", width: 64, height: 64),
const Gap(6),
LinearProgressIndicator(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
const Gap(20),
Text('appInitializing'.tr(), textAlign: TextAlign.center),
AppVersionLabel(),
],
),
).center(),
);
}
return widget.child;
}
}

View File

@ -125,10 +125,8 @@ class ChatChannelProvider extends ChangeNotifier {
final channelBox = await Hive.openBox<SnChatMessage>(
'${ChatMessageController.kChatMessageBoxPrefix}${channel.id}',
);
final lastMessage = channelBox.isNotEmpty
? channelBox.values
.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b)
: null;
final lastMessage =
channelBox.isNotEmpty ? channelBox.values.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) : null;
if (lastMessage != null) result.add(lastMessage);
channelBox.close();
}

51
lib/providers/config.dart Normal file
View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/widget.dart';
const kAtkStoreKey = 'nex_user_atk';
const kRtkStoreKey = 'nex_user_rtk';
const kNetworkServerDefault = 'https://api.sn.solsynth.dev';
const kNetworkServerStoreKey = 'app_server_url';
const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none,
'settingsImageQualityLow': FilterQuality.low,
'settingsImageQualityMedium': FilterQuality.medium,
'settingsImageQualityHigh': FilterQuality.high,
};
class ConfigProvider extends ChangeNotifier {
late final SharedPreferences prefs;
late final HomeWidgetProvider _home;
ConfigProvider(BuildContext context) {
_home = context.read<HomeWidgetProvider>();
}
Future<void> initialize() async {
prefs = await SharedPreferences.getInstance();
}
FilterQuality get imageQuality {
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
}
String get serverUrl {
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
}
set serverUrl(String url) {
prefs.setString(kNetworkServerStoreKey, url);
_home.saveWidgetData("nex_server_url", url);
}
String? updatableVersion;
void setUpdate(String newVersion) {
updatableVersion = newVersion;
notifyListeners();
}
}

View 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;
}
}
}

View File

@ -16,14 +16,6 @@ class NotificationProvider extends ChangeNotifier {
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!");
});
}
Future<void> registerPushNotifications() async {

View File

@ -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(
@ -60,6 +70,14 @@ class SnPostContentProvider {
return out;
}
Future<List<SnPost>> listRecommendations() async {
final resp = await _sn.client.get('/cgi/co/recommendations');
final out = _preloadRelatedDataInBatch(
List.from(resp.data.map((ele) => SnPost.fromJson(ele))),
);
return out;
}
Future<(List<SnPost>, int)> listPosts({
int take = 10,
int offset = 0,

View File

@ -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,

View File

@ -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,101 @@ 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;
if (!context.mounted) return;
_home.saveWidgetData("nex_server_url", client.options.baseUrl);
});
}
static Future<Dio> createOffContextClient() async {
final prefs = await SharedPreferences.getInstance();
final client = Dio();
client.interceptors.add(RetryInterceptor(
dio: client,
retries: 3,
retryDelays: const [
Duration(milliseconds: 300),
Duration(milliseconds: 1000),
Duration(milliseconds: 3000),
],
));
final ua = await _getUserAgent();
client.interceptors.add(
InterceptorsWrapper(
onRequest: (
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), prefs.getString(kRtkStoreKey), (atk, rtk) {
prefs.setString(kAtkStoreKey, atk);
prefs.setString(kRtkStoreKey, rtk);
});
if (atk != null) {
options.headers['Authorization'] = 'Bearer $atk';
}
options.headers['User-Agent'] = ua;
return handler.next(options);
},
),
);
client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
return client;
}
static Future<String> _getUserAgent() async {
final String platformInfo;
if (kIsWeb) {
final deviceInfo = await DeviceInfoPlugin().webBrowserInfo;
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 +158,6 @@ class SnNetworkProvider {
}
try {
var atk = _prefs.getString(kAtkStoreKey);
if (atk != null) {
final atkParts = atk.split('.');
if (atkParts.length != 3) {
@ -96,7 +183,13 @@ class SnNetworkProvider {
final exp = jsonDecode(payload)['exp'];
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
log('Access token need refresh, doing it at ${DateTime.now()}');
atk = await refreshToken();
final result = await _refreshToken(client.options.baseUrl, rtk);
if (result == null) {
atk = null;
} else {
atk = result.$1;
onRefresh?.call(atk, result.$2);
}
}
if (atk != null) {
@ -134,24 +227,32 @@ class SnNetworkProvider {
Future<String?> refreshToken() async {
final rtk = _prefs.getString(kRtkStoreKey);
final result = await _refreshToken(client.options.baseUrl, rtk);
if (result == null) return null;
_prefs.setString(kAtkStoreKey, result.$1);
_prefs.setString(kRtkStoreKey, result.$2);
return result.$1;
}
static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async {
if (rtk == null) return null;
final dio = Dio();
dio.options.baseUrl = client.options.baseUrl;
dio.options.baseUrl = baseUrl;
final resp = await dio.post('/cgi/id/auth/token', data: {
'grant_type': 'refresh_token',
'refresh_token': rtk,
});
final atk = resp.data['access_token'];
final nRtk = resp.data['refresh_token'];
setTokenPair(atk, nRtk);
final String atk = resp.data['access_token'];
final String nRtk = resp.data['refresh_token'];
return atk;
return (atk, nRtk);
}
void setBaseUrl(String url) {
_config.serverUrl = url;
client.options.baseUrl = url;
}
}

View File

@ -1,9 +1,12 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/widget.dart';
import 'package:surface/types/account.dart';
class UserProvider extends ChangeNotifier {
@ -11,24 +14,29 @@ class UserProvider extends ChangeNotifier {
SnAccount? user;
late final SnNetworkProvider _sn;
late final HomeWidgetProvider _home;
late final ConfigProvider _config;
UserProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_home = context.read<HomeWidgetProvider>();
_config = context.read<ConfigProvider>();
}
Future<String?> get atk async {
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) {
if (value != null) {
log('Logged in as @${value.name}');
_home.saveWidgetData('user', value.toJson());
}
});
}

View File

@ -23,16 +23,14 @@ 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 {
@ -79,6 +77,7 @@ class WebSocketProvider extends ChangeNotifier {
if (conn != null) {
conn!.sink.close();
}
conn = null;
isConnected = false;
notifyListeners();
}

60
lib/providers/widget.dart Normal file
View 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 (!kIsWeb && 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",
);
}

View File

@ -28,6 +28,7 @@ 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/types/post.dart';
import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_background.dart';
@ -58,7 +59,6 @@ final _appRoutes = [
path: '/write/:mode',
name: 'postEditor',
builder: (context, state) => AppBackground(
isLessOptimization: true,
child: PostEditorScreen(
mode: state.pathParameters['mode']!,
postEditId: int.tryParse(
@ -70,6 +70,7 @@ final _appRoutes = [
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtraProps?,
),
),
),
@ -77,7 +78,6 @@ final _appRoutes = [
path: '/search',
name: 'postSearch',
builder: (context, state) => const AppBackground(
isLessOptimization: true,
child: PostSearchScreen(),
),
),
@ -119,7 +119,6 @@ final _appRoutes = [
path: '/:scope/:alias',
name: 'chatRoom',
builder: (context, state) => AppBackground(
isLessOptimization: true,
child: ChatRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
@ -159,7 +158,6 @@ final _appRoutes = [
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: AppBackground(
isLessOptimization: true,
child: child,
),
);
@ -195,7 +193,6 @@ final _appRoutes = [
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: AppBackground(
isLessOptimization: true,
child: child,
),
);
@ -320,7 +317,9 @@ final appRouter = GoRouter(
routes: [
ShellRoute(
routes: _appRoutes,
builder: (context, state, child) => AppRootScaffold(body: child),
builder: (context, state, child) => AppRootScaffold(
body: AppSharingListener(child: child),
),
),
],
);

View File

@ -39,7 +39,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
void _showAbuseReportDialog() {
showDialog(
context: context,
builder: (context) => _AbuseReportDialog(),
builder: (context) => AbuseReportDialog(),
).then((value) {
if (value == true && mounted) {
_fetchReports();
@ -91,19 +91,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 +154,7 @@ class _AbuseReportDialogState extends State<_AbuseReportDialog> {
const Gap(12),
TextField(
controller: _resourceController,
readOnly: widget.resourceLocation != null,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),

View File

@ -2,7 +2,7 @@ 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';
@ -118,19 +118,19 @@ class _AuthorizedAccountScreen extends StatelessWidget {
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();
});
);
if (!confirm) return;
if (!context.mounted) return;
ua.logoutUser();
final ws = context.read<WebSocketProvider>();
ws.disconnect();
await Hive.deleteFromDisk();
await Hive.initFlutter();
},
),
ListTile(

View File

@ -3,13 +3,17 @@ 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: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/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/post.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart';
@ -73,6 +77,98 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
}
}
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;
@ -88,8 +184,19 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
@override
void initState() {
super.initState();
_fetchAccount().then((_) {
_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);
}
@ -131,7 +238,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
TextSpan(
text: _account!.nick,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.white,
color: Theme.of(context).appBarTheme.foregroundColor!,
shadows: labelShadows,
),
),
@ -139,7 +246,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
TextSpan(
text: '@${_account!.name}',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Colors.white,
color: Theme.of(context).appBarTheme.foregroundColor!,
shadows: labelShadows,
),
),
@ -205,6 +312,57 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
],
),
),
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),
@ -346,7 +504,31 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
),
],
),
)
),
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).pushNamed(
'postPublisher',
pathParameters: {'name': ele.name},
);
},
);
},
),
],
),
);

View File

@ -31,7 +31,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
setState(() => _isBusy = true);
try {
final resp = await sn.client.get('/cgi/co/publishers');
final resp = await sn.client.get('/cgi/co/publishers/me');
final List<SnPublisher> out = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);

View File

@ -1,6 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
@ -69,9 +67,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
{},
);
_otherMember = _channel!.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
}
} catch (err) {
if (!mounted) return;
@ -90,6 +88,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
options: Options(
validateStatus: (status) => status != null && status < 500,
receiveTimeout: const Duration(seconds: 60),
sendTimeout: const Duration(seconds: 60),
),
);
if (resp.statusCode == 200) {
@ -97,6 +97,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
}
} catch (err) {
if (!mounted) return;
print((err as DioException).response?.data);
context.showErrorDialog(err);
} finally {
setState(() => _isCalling = false);
@ -108,17 +109,19 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.post(
await sn.client.post(
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls',
options: Options(
sendTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
),
);
log(jsonDecode(resp.data));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
if (_ongoingCall == null) {
// ignore the error because the call is already ongoing
context.showErrorDialog(err);
}
} finally {
setState(() => _isCalling = false);
}
@ -155,7 +158,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
GoRouter.of(context).pushNamed(
'chatCallRoom',
pathParameters: {
'scope': _channel!.realm!.alias,
'scope': _channel!.realm?.alias ?? 'global',
'alias': _channel!.alias,
},
);

View File

@ -1,18 +1,29 @@
import 'dart:io';
import 'dart:math' as math;
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:flutter/material.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/check_in.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/post/post_item.dart';
import '../providers/widget.dart';
class HomeScreenDashEntry {
final String name;
@ -36,10 +47,20 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
static const List<HomeScreenDashEntry> kCards = [
HomeScreenDashEntry(
name: 'dashEntryRecommendation',
cols: 2,
rows: 2,
child: _HomeDashRecommendationPostWidget(),
),
HomeScreenDashEntry(
name: 'dashEntryCheckIn',
child: _HomeDashCheckInWidget(),
),
HomeScreenDashEntry(
name: 'dashEntryNotification',
child: _HomeDashNotificationWidget(),
),
];
@override
@ -52,21 +73,17 @@ class _HomeScreenState extends State<HomeScreen> {
body: LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: constraints.maxWidth > 640
? Alignment.center
: Alignment.topCenter,
alignment: constraints.maxWidth > 640 ? Alignment.center : Alignment.topCenter,
child: Container(
constraints: const BoxConstraints(maxWidth: 640),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: constraints.maxWidth > 640
? MainAxisAlignment.center
: MainAxisAlignment.start,
mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
if (constraints.maxWidth <= 640) const Gap(8),
_HomeDashSpecialDayWidget().padding(top: 8, horizontal: 8),
StaggeredGrid.count(
crossAxisCount: 2,
_HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8),
_HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)),
StaggeredGrid.extent(
maxCrossAxisExtent: 280,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: kCards.map((card) {
@ -78,7 +95,7 @@ class _HomeScreenState extends State<HomeScreen> {
}).toList(),
).padding(horizontal: 8),
],
),
).padding(vertical: 8),
),
),
);
@ -88,6 +105,52 @@ class _HomeScreenState extends State<HomeScreen> {
}
}
class _HomeDashUpdateWidget extends StatelessWidget {
final EdgeInsets? padding;
const _HomeDashUpdateWidget({super.key, this.padding});
@override
Widget build(BuildContext context) {
final config = context.watch<ConfigProvider>();
return ListenableBuilder(
listenable: config,
builder: (context, _) {
if (config.updatableVersion != null) {
return Container(
padding: padding,
child: Card(
child: ListTile(
leading: Icon(Symbols.update),
title: Text('updateAvailable').tr(),
subtitle: Text(config.updatableVersion!),
trailing: (kIsWeb || Platform.isWindows || Platform.isLinux)
? null
: IconButton(
icon: const Icon(Symbols.arrow_right_alt),
onPressed: () {
final model = UpdateModel(
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
'solian-app-release-${config.updatableVersion!}.apk',
'ic_notification',
'https://apps.apple.com/us/app/solian/id6499032345',
);
AzhonAppUpdate.update(model);
context.showSnackbar('updateOngoing'.tr());
},
),
),
),
);
}
return SizedBox.shrink();
},
);
}
}
class _HomeDashSpecialDayWidget extends StatelessWidget {
const _HomeDashSpecialDayWidget({super.key});
@ -96,10 +159,10 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
final ua = context.watch<UserProvider>();
final today = DateTime.now();
final birthday = ua.user?.profile?.birthday?.toLocal();
final isBirthday = birthday != null &&
birthday.day == today.day &&
birthday.month == today.month;
final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month;
return Column(
spacing: 8,
children: [
if (isBirthday)
Card(
@ -108,6 +171,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']),
),
).padding(bottom: 8),
if (today.month == 12 && today.day == 25)
Card(
child: ListTile(
leading: Text('🎄').fontSize(24),
title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']),
),
),
if (today.month == 1 && today.day == 1)
Card(
child: ListTile(
leading: Text('🎉').fontSize(24),
title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']),
),
),
],
);
}
@ -132,8 +209,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final home = context.read<HomeWidgetProvider>();
final resp = await sn.client.get('/cgi/id/check-in/today');
_todayRecord = SnCheckInRecord.fromJson(resp.data);
home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
} finally {
setState(() => _isBusy = false);
}
@ -143,8 +222,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final home = context.read<HomeWidgetProvider>();
final resp = await sn.client.post('/cgi/id/check-in');
_todayRecord = SnCheckInRecord.fromJson(resp.data);
home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -154,21 +235,16 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
}
Widget _buildDetailChunk(int index, bool positive) {
final prefix =
positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint';
final mod =
positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
final prefix = positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint';
final mod = positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
final pos = math.max(1, _todayRecord!.resultModifiers[index] % mod);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
prefix.tr(args: ['$prefix$pos'.tr()]),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
).tr(),
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
),
Text(
'$prefix${pos}Description',
style: Theme.of(context).textTheme.bodyMedium,
@ -202,10 +278,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
else
Text(
'dailyCheckEverythingIsNegative',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
).tr(),
const Gap(8),
if (_todayRecord?.resultTier != 4)
@ -221,10 +294,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
else
Text(
'dailyCheckEverythingIsPositive',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
).tr(),
],
),
@ -338,14 +408,34 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
}
}
class _HomeDashLinkWidget extends StatelessWidget {
final String title;
final String subtitle;
const _HomeDashLinkWidget({
super.key,
required this.title,
required this.subtitle,
});
class _HomeDashNotificationWidget extends StatefulWidget {
const _HomeDashNotificationWidget({super.key});
@override
State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState();
}
class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget> {
int? _count;
Future<void> _fetchNotificationCount() async {
final ua = context.read<UserProvider>();
if (!ua.isAuthorized) {
setState(() => _count = 0);
return;
}
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/notifications/count');
_count = resp.data['count'];
setState(() {});
}
@override
void initState() {
super.initState();
_fetchNotificationCount();
}
@override
Widget build(BuildContext context) {
@ -358,11 +448,11 @@ class _HomeDashLinkWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
'notification',
style: Theme.of(context).textTheme.titleLarge,
),
).tr(),
Text(
subtitle,
_count == null ? 'loading'.tr() : 'notificationUnreadCount'.plural(_count ?? 0),
style: Theme.of(context).textTheme.bodyLarge,
),
],
@ -377,7 +467,9 @@ class _HomeDashLinkWidget extends StatelessWidget {
),
child: IconButton(
icon: const Icon(Symbols.arrow_right_alt),
onPressed: () {},
onPressed: () {
GoRouter.of(context).goNamed('notification');
},
),
),
)
@ -386,3 +478,87 @@ class _HomeDashLinkWidget extends StatelessWidget {
);
}
}
class _HomeDashRecommendationPostWidget extends StatefulWidget {
const _HomeDashRecommendationPostWidget({super.key});
@override
State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState();
}
class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendationPostWidget> {
bool _isBusy = false;
List<SnPost>? _posts;
Future<void> _fetchRecommendationPosts() async {
setState(() => _isBusy = true);
try {
final pt = context.read<SnPostContentProvider>();
final home = context.read<HomeWidgetProvider>();
_posts = await pt.listRecommendations();
home.saveWidgetData('post_featured', _posts!.first.toJson());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchRecommendationPosts();
}
@override
Widget build(BuildContext context) {
if (_isBusy) {
return Card(
child: CircularProgressIndicator().center(),
);
}
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Symbols.star),
const Gap(8),
Text(
'postRecommendation',
style: Theme.of(context).textTheme.titleLarge,
).tr()
],
).padding(horizontal: 18, top: 12, bottom: 8),
Expanded(
child: PageView.builder(
scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
}),
itemCount: _posts?.length ?? 0,
itemBuilder: (context, index) {
return SingleChildScrollView(
child: GestureDetector(
child: PostItem(
data: _posts![index],
showMenu: false,
).padding(bottom: 8),
onTap: () {
GoRouter.of(context).pushNamed('postDetail', pathParameters: {
'slug': _posts![index].id.toString(),
});
},
),
);
},
),
),
],
),
);
}
}

View File

@ -84,12 +84,16 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
text: TextSpan(children: [
TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
)

View File

@ -23,11 +23,26 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:provider/provider.dart';
class PostEditorExtraProps {
final String? text;
final String? title;
final String? description;
final List<PostWriteMedia>? attachments;
const PostEditorExtraProps({
this.text,
this.title,
this.description,
this.attachments,
});
}
class PostEditorScreen extends StatefulWidget {
final String mode;
final int? postEditId;
final int? postReplyId;
final int? postRepostId;
final PostEditorExtraProps? extraProps;
const PostEditorScreen({
super.key,
@ -35,6 +50,7 @@ class PostEditorScreen extends StatefulWidget {
required this.postEditId,
required this.postReplyId,
required this.postRepostId,
this.extraProps,
});
@override
@ -55,7 +71,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers');
final resp = await sn.client.get('/cgi/co/publishers/me');
_publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
);
@ -130,6 +146,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
replying: widget.postReplyId,
reposting: widget.postRepostId,
);
if (widget.extraProps != null) {
_writeController.contentController.text = widget.extraProps!.text ?? '';
_writeController.titleController.text = widget.extraProps!.title ?? '';
_writeController.descriptionController.text = widget.extraProps!.description ?? '';
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
}
}
@override
@ -149,12 +171,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
text: TextSpan(children: [
TextSpan(
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: PostWriteController.kTitleMap[widget.mode]!.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
),

View File

@ -145,6 +145,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(horizontal: 24),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onChanged: (value) {
_searchTerm = value;
},

View File

@ -14,42 +14,54 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/post.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/post/post_item.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../providers/relationship.dart';
import '../abuse_report.dart';
class PostPublisherScreen extends StatefulWidget {
final String name;
const PostPublisherScreen({super.key, required this.name});
@override
State<PostPublisherScreen> createState() => _PostPublisherScreenState();
}
class _PostPublisherScreenState extends State<PostPublisherScreen>
with SingleTickerProviderStateMixin {
class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController();
late final TabController _tabController =
TabController(length: 3, vsync: this);
late final TabController _tabController = TabController(length: 3, vsync: this);
SnPublisher? _publisher;
SnAccount? _account;
SnRelationship? _accountRelationship;
SnRealm? _realm;
Future<void> _fetchPublisher() async {
try {
final sn = context.read<SnNetworkProvider>();
final ud = context.read<UserDirectoryProvider>();
final rel = context.read<SnRelationshipProvider>();
final resp = await sn.client.get('/cgi/co/publishers/${widget.name}');
if (!mounted) return;
_publisher = SnPublisher.fromJson(resp.data);
_account = await ud.getAccount(_publisher?.accountId);
_accountRelationship = await rel.getRelationship(_account!.id);
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
_realm = SnRealm.fromJson(resp.data);
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err).then((_) {
if (mounted) Navigator.pop(context);
});
rethrow;
} finally {
setState(() {});
}
@ -114,14 +126,12 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble();
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return;
setState(() {
_appBarBlur =
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
});
}
@ -156,11 +166,72 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
}
}
bool _isWorking = false;
Future<void> _blockPublisher() async {
if (_isWorking) return;
final confirm = await context.showConfirmDialog(
'publisherBlockHint'.tr(args: ['@${_publisher?.name ?? 'unknown'.tr()}']),
'publisherBlockHintDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
setState(() => _isWorking = 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'.tr()}']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isWorking = false);
}
}
Future<void> _unblockPublisher() async {
if (_isWorking) return;
setState(() => _isWorking = 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'.tr()}']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isWorking = false);
}
}
void _updateFetchType() {
_posts.clear();
_fetchPosts();
}
void _showAbuseReportDialog() {
showDialog(
context: context,
builder: (context) => AbuseReportDialog(
resourceLocation: 'pub:${_publisher?.name}',
),
).then((value) {
if (value == true && mounted) {
_fetchPosts();
context.showSnackbar('abuseReportSubmitted'.tr());
}
});
}
@override
void initState() {
super.initState();
@ -215,21 +286,15 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
text: TextSpan(children: [
TextSpan(
text: _publisher!.nick,
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(
color: Colors.white,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
shadows: labelShadows,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: '@${_publisher!.name}',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Colors.white,
shadows: labelShadows,
),
@ -241,14 +306,19 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
? Stack(
fit: StackFit.expand,
children: [
UniversalImage(
sn.getAttachmentUrl(_publisher!.banner),
fit: BoxFit.cover,
height: imageHeight,
width: _appBarWidth,
cacheHeight: imageHeight,
cacheWidth: _appBarWidth,
),
if (_publisher!.banner.isNotEmpty)
UniversalImage(
sn.getAttachmentUrl(_publisher!.banner),
fit: BoxFit.cover,
height: imageHeight,
width: _appBarWidth,
cacheHeight: imageHeight,
cacheWidth: _appBarWidth,
)
else
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
),
Positioned(
top: 0,
left: 0,
@ -288,14 +358,11 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
const Gap(16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_publisher!.nick,
style: Theme.of(context)
.textTheme
.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
).bold(),
Text('@${_publisher!.name}').fontSize(13),
],
@ -306,9 +373,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
style: ButtonStyle(
elevation: WidgetStatePropertyAll(0),
),
onPressed: _isSubscribing
? null
: _toggleSubscription,
onPressed: _isSubscribing ? null : _toggleSubscription,
label: Text('subscribe').tr(),
icon: const Icon(Symbols.add),
)
@ -317,17 +382,54 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
style: ButtonStyle(
elevation: WidgetStatePropertyAll(0),
),
onPressed: _isSubscribing
? null
: _toggleSubscription,
onPressed: _isSubscribing ? null : _toggleSubscription,
label: Text('unsubscribe').tr(),
icon: const Icon(Symbols.remove),
),
PopupMenuButton(
padding: EdgeInsets.zero,
style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.flag),
const Gap(16),
Text('report').tr(),
],
),
onTap: () => _showAbuseReportDialog(),
),
if (_accountRelationship?.status != 2)
PopupMenuItem(
onTap: _blockPublisher,
child: Row(
children: [
const Icon(Symbols.block),
const Gap(16),
Text('friendBlock').tr(),
],
),
)
else
PopupMenuItem(
onTap: _unblockPublisher,
child: Row(
children: [
const Icon(Symbols.block),
const Gap(16),
Text('friendUnblock').tr(),
],
),
),
],
),
],
).padding(right: 8),
),
const Gap(12),
Text(_publisher!.description)
.padding(horizontal: 8),
Text(_publisher!.description).padding(horizontal: 8),
const Gap(12),
Column(
children: [
@ -335,10 +437,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
children: [
const Icon(Symbols.calendar_add_on),
const Gap(8),
Text('publisherJoinedAt').tr(args: [
DateFormat('y/M/d')
.format(_publisher!.createdAt)
]),
Text('publisherJoinedAt')
.tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]),
],
),
Row(
@ -346,11 +446,30 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
const Icon(Symbols.trending_up),
const Gap(8),
Text('publisherSocialPointTotal').plural(
_publisher!.totalUpvote -
_publisher!.totalDownvote,
_publisher!.totalUpvote - _publisher!.totalDownvote,
),
],
),
if (_realm != null)
Row(
children: [
const Icon(Symbols.group_work),
const Gap(8),
InkWell(
child: Text('publisherAffiliatedBy').tr(args: [
'@${_realm?.alias ?? 'unknown'}',
]),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': _realm!.alias},
);
},
),
const Gap(8),
AccountImage(content: _realm?.avatar, radius: 8),
],
),
Row(
children: [
const Icon(Symbols.tools_wrench),
@ -369,8 +488,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
},
),
const Gap(8),
AccountImage(
content: _account?.avatar, radius: 8),
AccountImage(content: _account?.avatar, radius: 8),
],
),
],
@ -447,6 +565,7 @@ class _PublisherPostList extends StatelessWidget {
final void Function() fetchPosts;
final void Function(int index, SnPost data) onChanged;
final void Function() onDeleted;
const _PublisherPostList({
super.key,
required this.isBusy,

View File

@ -6,16 +6,15 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/realm.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/loading_indicator.dart';
import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart';
import '../providers/userinfo.dart';
import '../widgets/unauthorized_hint.dart';
class RealmScreen extends StatefulWidget {
const RealmScreen({super.key});
@ -101,9 +100,7 @@ class _RealmScreenState extends State<RealmScreen> {
title: Text('screenRealm').tr(),
actions: [
IconButton(
icon: !_isCompactView
? const Icon(Symbols.view_list)
: const Icon(Symbols.view_module),
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
onPressed: () {
setState(() => _isCompactView = !_isCompactView);
},
@ -129,8 +126,7 @@ class _RealmScreenState extends State<RealmScreen> {
final realm = _realms![idx];
if (_isCompactView) {
return ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: realm.avatar,
fallbackWidget: const Icon(Symbols.group, size: 20),
@ -201,9 +197,7 @@ class _RealmScreenState extends State<RealmScreen> {
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context)
.colorScheme
.surfaceContainer,
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
@ -217,8 +211,7 @@ class _RealmScreenState extends State<RealmScreen> {
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget:
const Icon(Symbols.group, size: 24),
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
@ -228,10 +221,8 @@ class _RealmScreenState extends State<RealmScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(
Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(
Theme.of(context).textTheme.bodySmall!),
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],

View File

@ -13,8 +13,11 @@ import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../types/post.dart';
class RealmDetailScreen extends StatefulWidget {
final String alias;
const RealmDetailScreen({super.key, required this.alias});
@override
@ -32,6 +35,24 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
rethrow;
} finally {
setState(() {});
}
}
List<SnPublisher>? _publishers;
Future<void> _fetchPublishers() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers?realm=${widget.alias}');
_publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
);
} catch (err) {
if (mounted) context.showErrorDialog(err);
rethrow;
} finally {
setState(() {});
}
@ -40,7 +61,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
@override
void initState() {
super.initState();
_fetchRealm();
_fetchRealm().then((_) {
_fetchPublishers();
});
}
@override
@ -60,8 +83,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text(_realm?.name ?? 'loading'.tr()),
bottom: TabBar(
@ -77,7 +99,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
},
body: TabBarView(
children: [
_RealmDetailHomeWidget(realm: _realm),
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers),
_RealmMemberListWidget(realm: _realm),
_RealmSettingsWidget(
realm: _realm,
@ -95,7 +117,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
class _RealmDetailHomeWidget extends StatelessWidget {
final SnRealm? realm;
const _RealmDetailHomeWidget({super.key, required this.realm});
final List<SnPublisher>? publishers;
const _RealmDetailHomeWidget({super.key, required this.realm, this.publishers});
@override
Widget build(BuildContext context) {
@ -118,6 +142,31 @@ class _RealmDetailHomeWidget extends StatelessWidget {
).padding(horizontal: 24),
const Gap(16),
const Divider(),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: publishers?.length ?? 0,
itemBuilder: (context, idx) {
final ele = publishers![idx];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
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).pushNamed(
'postPublisher',
pathParameters: {'name': ele.name},
);
},
);
},
),
),
],
);
}
@ -125,6 +174,7 @@ class _RealmDetailHomeWidget extends StatelessWidget {
class _RealmMemberListWidget extends StatefulWidget {
final SnRealm? realm;
const _RealmMemberListWidget({super.key, this.realm});
@override
@ -143,12 +193,10 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
try {
final ud = context.read<UserDirectoryProvider>();
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get(
'/cgi/id/realms/${widget.realm!.alias}/members',
queryParameters: {
'take': 10,
'offset': 0,
});
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
'take': 10,
'offset': 0,
});
final out = List<SnRealmMember>.from(
resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [],
@ -236,12 +284,10 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
fallbackWidget: const Icon(Symbols.group, size: 24),
),
title: Text(
ud.getAccountFromCache(member.accountId)?.nick ??
'unknown'.tr(),
ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
),
subtitle: Text(
ud.getAccountFromCache(member.accountId)?.name ??
'unknown'.tr(),
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
),
trailing: IconButton(
icon: const Icon(Symbols.person_remove),
@ -257,6 +303,7 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
class _NewRealmMemberWidget extends StatefulWidget {
final SnRealm realm;
const _NewRealmMemberWidget({super.key, required this.realm});
@override
@ -321,8 +368,7 @@ class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> {
child: IconButton(
onPressed: _isBusy ? null : () => _performAction(),
icon: Icon(Symbols.send),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
),
),
@ -337,8 +383,8 @@ class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> {
class _RealmSettingsWidget extends StatefulWidget {
final SnRealm? realm;
final Function() onUpdate;
const _RealmSettingsWidget(
{super.key, required this.realm, required this.onUpdate});
const _RealmSettingsWidget({super.key, required this.realm, required this.onUpdate});
@override
State<_RealmSettingsWidget> createState() => _RealmSettingsWidgetState();
@ -382,6 +428,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
return Column(
children: [
const Gap(16),
ListTile(
leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right),

View File

@ -1,10 +1,10 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -12,6 +12,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/theme.dart';
import 'package:surface/theme.dart';
@ -25,7 +26,7 @@ class SettingsScreen extends StatefulWidget {
}
class _SettingsScreenState extends State<SettingsScreen> {
SharedPreferences? _prefs;
late final SharedPreferences _prefs;
String _docBasepath = '/';
final TextEditingController _serverUrlController = TextEditingController();
@ -39,12 +40,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {});
}
});
SharedPreferences.getInstance().then((prefs) {
setState(() {
_prefs = prefs;
_serverUrlController.text = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
});
});
final config = context.read<ConfigProvider>();
_prefs = config.prefs;
_serverUrlController.text = config.serverUrl;
}
@override
@ -60,6 +58,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
return Scaffold(
body: SingleChildScrollView(
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
@ -78,7 +77,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (image == null) return;
await File(image.path).copy('$_docBasepath/app_background_image');
_prefs?.setBool('has_background_image', true);
_prefs.setBool('has_background_image', true);
setState(() {});
},
@ -99,29 +98,28 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Symbols.chevron_right),
onTap: () {
File('$_docBasepath/app_background_image').deleteSync();
_prefs?.remove('has_background_image');
_prefs.remove('has_background_image');
setState(() {});
},
);
}),
if (_prefs != null)
CheckboxListTile(
title: Text('settingsThemeMaterial3').tr(),
subtitle: Text('settingsThemeMaterial3Description').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases),
value: _prefs!.getBool(kMaterialYouToggleStoreKey) ?? false,
onChanged: (value) {
setState(() {
_prefs!.setBool(
kMaterialYouToggleStoreKey,
value ?? false,
);
});
final th = context.watch<ThemeProvider>();
th.reloadTheme(useMaterial3: value ?? false);
},
),
CheckboxListTile(
title: Text('settingsThemeMaterial3').tr(),
subtitle: Text('settingsThemeMaterial3Description').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases),
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
onChanged: (value) {
setState(() {
_prefs.setBool(
kMaterialYouToggleStoreKey,
value ?? false,
);
});
final th = context.watch<ThemeProvider>();
th.reloadTheme(useMaterial3: value ?? false);
},
),
],
),
Column(
@ -139,7 +137,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
icon: const Icon(Symbols.save),
onPressed: () {
sn.setBaseUrl(_serverUrlController.text);
_prefs?.setString(
_prefs.setString(
kNetworkServerStoreKey,
_serverUrlController.text,
);
@ -182,7 +180,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
onChanged: (String? value) {
if (value == null) return;
_serverUrlController.text = value;
_prefs?.setString(kNetworkServerStoreKey, value);
_prefs.setString(kNetworkServerStoreKey, value);
context.showSnackbar('settingsNetworkServerSaved'.tr());
setState(() {});
},
@ -208,13 +206,56 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Symbols.chevron_right),
onTap: () {
_serverUrlController.text = kNetworkServerDefault;
_prefs?.remove(kNetworkServerStoreKey);
_prefs.remove(kNetworkServerStoreKey);
context.showSnackbar('settingsNetworkServerSaved'.tr());
setState(() {});
},
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('settingsPerformance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
ListTile(
title: Text('settingsImageQuality').tr(),
subtitle: Text('settingsImageQualityDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.image),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<FilterQuality>(
value: kImageQualityLevel.values.elementAtOrNull(_prefs.getInt('app_image_quality') ?? 3) ??
FilterQuality.high,
isExpanded: true,
items: kImageQualityLevel.entries
.map(
(item) => DropdownMenuItem<FilterQuality>(
value: item.value,
child: Text(item.key).tr().fontSize(14),
),
)
.toList(),
onChanged: (FilterQuality? value) {
if (value == null) return;
_prefs.setInt('app_image_quality', kImageQualityLevel.values.toList().indexOf(value));
setState(() {});
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
height: 40,
width: 160,
),
menuItemStyleData: const MenuItemStyleData(
height: 60,
),
),
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -231,7 +272,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
],
),
].expand((ele) => [ele, const Gap(16)]).toList(),
],
).padding(vertical: 20),
),
);

122
lib/screens/sharing.dart Normal file
View File

@ -0,0 +1,122 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/screens/post/post_editor.dart';
class AppSharingListener extends StatefulWidget {
final Widget child;
const AppSharingListener({super.key, required this.child});
@override
State<AppSharingListener> createState() => _AppSharingListenerState();
}
class _AppSharingListenerState extends State<AppSharingListener> {
late StreamSubscription _shareIntentSubscription;
void _gotoPost(Iterable<SharedMediaFile> value) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('shareIntent').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('shareIntentDescription').tr(),
const Gap(8),
Card(
child: Column(
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
leading: Icon(Icons.post_add),
trailing: const Icon(Icons.chevron_right),
title: Text('shareIntentPostStory').tr(),
onTap: () {
GoRouter.of(context).pushNamed(
'postEditor',
pathParameters: {
'mode': 'stories',
},
extra: PostEditorExtraProps(
text: value
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path).join('\n'),
attachments: value
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
),
);
Navigator.pop(context);
},
),
],
),
)
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('dialogDismiss').tr(),
)
],
),
);
});
}
void _initialize() async {
_shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) {
if (value.isEmpty) return;
if (mounted) {
_gotoPost(value);
}
}, onError: (err) {
log("[ShareIntent] Unable to subscribe: $err");
});
}
void _initialHandle() {
ReceiveSharingIntent.instance.getInitialMedia().then((value) {
if (value.isEmpty) return;
if (mounted) {
_gotoPost(value);
}
ReceiveSharingIntent.instance.reset();
});
}
@override
void initState() {
super.initState();
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
_initialize();
_initialHandle();
}
}
@override
void dispose() {
_shareIntentSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}

View File

@ -1,8 +1,4 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
const kMaterialYouToggleStoreKey = 'app_theme_material_you';

28
lib/types/link.dart Normal file
View File

@ -0,0 +1,28 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'link.g.dart';
part 'link.freezed.dart';
@freezed
class SnLinkMeta with _$SnLinkMeta {
const SnLinkMeta._();
const factory SnLinkMeta({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String entryId,
required String? icon,
required String url,
required String? title,
required String? image,
required String? video,
required String? audio,
required String? description,
required String? siteName,
required String? type,
}) = _SnLinkMeta;
factory SnLinkMeta.fromJson(Map<String, dynamic> json) => _$SnLinkMetaFromJson(json);
}

450
lib/types/link.freezed.dart Normal file
View File

@ -0,0 +1,450 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'link.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
SnLinkMeta _$SnLinkMetaFromJson(Map<String, dynamic> json) {
return _SnLinkMeta.fromJson(json);
}
/// @nodoc
mixin _$SnLinkMeta {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
DateTime? get deletedAt => throw _privateConstructorUsedError;
String get entryId => throw _privateConstructorUsedError;
String? get icon => throw _privateConstructorUsedError;
String get url => throw _privateConstructorUsedError;
String? get title => throw _privateConstructorUsedError;
String? get image => throw _privateConstructorUsedError;
String? get video => throw _privateConstructorUsedError;
String? get audio => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
String? get siteName => throw _privateConstructorUsedError;
String? get type => throw _privateConstructorUsedError;
/// Serializes this SnLinkMeta to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnLinkMeta
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnLinkMetaCopyWith<SnLinkMeta> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnLinkMetaCopyWith<$Res> {
factory $SnLinkMetaCopyWith(
SnLinkMeta value, $Res Function(SnLinkMeta) then) =
_$SnLinkMetaCopyWithImpl<$Res, SnLinkMeta>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String entryId,
String? icon,
String url,
String? title,
String? image,
String? video,
String? audio,
String? description,
String? siteName,
String? type});
}
/// @nodoc
class _$SnLinkMetaCopyWithImpl<$Res, $Val extends SnLinkMeta>
implements $SnLinkMetaCopyWith<$Res> {
_$SnLinkMetaCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnLinkMeta
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? entryId = null,
Object? icon = freezed,
Object? url = null,
Object? title = freezed,
Object? image = freezed,
Object? video = freezed,
Object? audio = freezed,
Object? description = freezed,
Object? siteName = freezed,
Object? type = freezed,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
entryId: null == entryId
? _value.entryId
: entryId // ignore: cast_nullable_to_non_nullable
as String,
icon: freezed == icon
? _value.icon
: icon // ignore: cast_nullable_to_non_nullable
as String?,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
image: freezed == image
? _value.image
: image // ignore: cast_nullable_to_non_nullable
as String?,
video: freezed == video
? _value.video
: video // ignore: cast_nullable_to_non_nullable
as String?,
audio: freezed == audio
? _value.audio
: audio // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
siteName: freezed == siteName
? _value.siteName
: siteName // ignore: cast_nullable_to_non_nullable
as String?,
type: freezed == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnLinkMetaImplCopyWith<$Res>
implements $SnLinkMetaCopyWith<$Res> {
factory _$$SnLinkMetaImplCopyWith(
_$SnLinkMetaImpl value, $Res Function(_$SnLinkMetaImpl) then) =
__$$SnLinkMetaImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String entryId,
String? icon,
String url,
String? title,
String? image,
String? video,
String? audio,
String? description,
String? siteName,
String? type});
}
/// @nodoc
class __$$SnLinkMetaImplCopyWithImpl<$Res>
extends _$SnLinkMetaCopyWithImpl<$Res, _$SnLinkMetaImpl>
implements _$$SnLinkMetaImplCopyWith<$Res> {
__$$SnLinkMetaImplCopyWithImpl(
_$SnLinkMetaImpl _value, $Res Function(_$SnLinkMetaImpl) _then)
: super(_value, _then);
/// Create a copy of SnLinkMeta
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? entryId = null,
Object? icon = freezed,
Object? url = null,
Object? title = freezed,
Object? image = freezed,
Object? video = freezed,
Object? audio = freezed,
Object? description = freezed,
Object? siteName = freezed,
Object? type = freezed,
}) {
return _then(_$SnLinkMetaImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
entryId: null == entryId
? _value.entryId
: entryId // ignore: cast_nullable_to_non_nullable
as String,
icon: freezed == icon
? _value.icon
: icon // ignore: cast_nullable_to_non_nullable
as String?,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
image: freezed == image
? _value.image
: image // ignore: cast_nullable_to_non_nullable
as String?,
video: freezed == video
? _value.video
: video // ignore: cast_nullable_to_non_nullable
as String?,
audio: freezed == audio
? _value.audio
: audio // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
siteName: freezed == siteName
? _value.siteName
: siteName // ignore: cast_nullable_to_non_nullable
as String?,
type: freezed == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnLinkMetaImpl extends _SnLinkMeta {
const _$SnLinkMetaImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.entryId,
required this.icon,
required this.url,
required this.title,
required this.image,
required this.video,
required this.audio,
required this.description,
required this.siteName,
required this.type})
: super._();
factory _$SnLinkMetaImpl.fromJson(Map<String, dynamic> json) =>
_$$SnLinkMetaImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String entryId;
@override
final String? icon;
@override
final String url;
@override
final String? title;
@override
final String? image;
@override
final String? video;
@override
final String? audio;
@override
final String? description;
@override
final String? siteName;
@override
final String? type;
@override
String toString() {
return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnLinkMetaImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.entryId, entryId) || other.entryId == entryId) &&
(identical(other.icon, icon) || other.icon == icon) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.image, image) || other.image == image) &&
(identical(other.video, video) || other.video == video) &&
(identical(other.audio, audio) || other.audio == audio) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.siteName, siteName) ||
other.siteName == siteName) &&
(identical(other.type, type) || other.type == type));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
entryId,
icon,
url,
title,
image,
video,
audio,
description,
siteName,
type);
/// Create a copy of SnLinkMeta
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith =>
__$$SnLinkMetaImplCopyWithImpl<_$SnLinkMetaImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnLinkMetaImplToJson(
this,
);
}
}
abstract class _SnLinkMeta extends SnLinkMeta {
const factory _SnLinkMeta(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String entryId,
required final String? icon,
required final String url,
required final String? title,
required final String? image,
required final String? video,
required final String? audio,
required final String? description,
required final String? siteName,
required final String? type}) = _$SnLinkMetaImpl;
const _SnLinkMeta._() : super._();
factory _SnLinkMeta.fromJson(Map<String, dynamic> json) =
_$SnLinkMetaImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
DateTime? get deletedAt;
@override
String get entryId;
@override
String? get icon;
@override
String get url;
@override
String? get title;
@override
String? get image;
@override
String? get video;
@override
String? get audio;
@override
String? get description;
@override
String? get siteName;
@override
String? get type;
/// Create a copy of SnLinkMeta
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith =>
throw _privateConstructorUsedError;
}

45
lib/types/link.g.dart Normal file
View File

@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'link.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SnLinkMetaImpl _$$SnLinkMetaImplFromJson(Map<String, dynamic> json) =>
_$SnLinkMetaImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
entryId: json['entry_id'] as String,
icon: json['icon'] as String?,
url: json['url'] as String,
title: json['title'] as String?,
image: json['image'] as String?,
video: json['video'] as String?,
audio: json['audio'] as String?,
description: json['description'] as String?,
siteName: json['site_name'] as String?,
type: json['type'] as String?,
);
Map<String, dynamic> _$$SnLinkMetaImplToJson(_$SnLinkMetaImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'entry_id': instance.entryId,
'icon': instance.icon,
'url': instance.url,
'title': instance.title,
'image': instance.image,
'video': instance.video,
'audio': instance.audio,
'description': instance.description,
'site_name': instance.siteName,
'type': instance.type,
};

View File

@ -1,5 +1,6 @@
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@ -14,19 +15,21 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data;
final bool bordered;
final bool noGrow;
final bool isFlatted;
final double? maxHeight;
final EdgeInsets? listPadding;
const AttachmentList({
super.key,
required this.data,
this.bordered = false,
this.noGrow = false,
this.isFlatted = false,
this.maxHeight,
this.listPadding,
});
static const BorderRadius kDefaultRadius =
BorderRadius.all(Radius.circular(8));
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
@override
State<AttachmentList> createState() => _AttachmentListState();
@ -44,9 +47,8 @@ class _AttachmentListState extends State<AttachmentList> {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, layoutConstraints) {
final borderSide = widget.bordered
? BorderSide(width: 1, color: Theme.of(context).dividerColor)
: BorderSide.none;
final borderSide =
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final constraints = BoxConstraints(
minWidth: 80,
@ -56,14 +58,13 @@ class _AttachmentListState extends State<AttachmentList> {
if (widget.data.isEmpty) return const SizedBox.shrink();
if (widget.data.length == 1) {
final singleAspectRatio =
widget.data[0]?.metadata['ratio']?.toDouble() ??
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
'audio' => 16 / 9,
'video' => 16 / 9,
_ => 1,
}
.toDouble();
final singleAspectRatio = widget.data[0]?.metadata['ratio']?.toDouble() ??
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
'audio' => 16 / 9,
'video' => 16 / 9,
_ => 1,
}
.toDouble();
return Container(
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
@ -79,8 +80,7 @@ class _AttachmentListState extends State<AttachmentList> {
child: GestureDetector(
child: Builder(
builder: (context) {
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) ||
widget.noGrow) {
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
return Padding(
// Single child list-like displaying
padding: widget.listPadding ?? EdgeInsets.zero,
@ -129,8 +129,39 @@ class _AttachmentListState extends State<AttachmentList> {
);
}
if (widget.isFlatted) {
return Wrap(
spacing: 4,
runSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => AspectRatio(
aspectRatio: (ele?.metadata['ratio'] ?? 1).toDouble(),
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
),
),
),
),
)
.toList(),
);
}
return AspectRatio(
aspectRatio: widget.data.firstOrNull?.metadata['ratio'] ?? 1,
aspectRatio: (widget.data.firstOrNull?.metadata['ratio'] ?? 1).toDouble(),
child: Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ScrollConfiguration(
@ -142,14 +173,12 @@ class _AttachmentListState extends State<AttachmentList> {
return Container(
constraints: constraints,
child: AspectRatio(
aspectRatio: widget.data[idx]?.metadata['ratio'] ?? 1,
aspectRatio: (widget.data[idx]?.metadata['ratio'] ?? 1).toDouble(),
child: GestureDetector(
onTap: () {
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data
.where((ele) => ele != null)
.cast(),
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gal/gal.dart';
@ -64,10 +65,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
return;
}
if (!await Gal.hasAccess(toAlbum: true)) {
if (!await Gal.requestAccess(toAlbum: true)) return;
}
setState(() => _isDownloading = true);
var extName = extension(item.name);
@ -83,7 +80,17 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
bool isSuccess = false;
try {
await Gal.putImage(imagePath, album: 'Solar Network');
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
if (!await Gal.hasAccess(toAlbum: true)) {
if (!await Gal.requestAccess(toAlbum: true)) return;
}
await Gal.putImage(imagePath, album: 'Solar Network');
} else {
await FileSaver.instance.saveFile(
name: item.name,
file: File(imagePath),
);
}
setState(() {
isSuccess = true;
_isDownloading = false;
@ -96,11 +103,13 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
if (!mounted) return;
context.showSnackbar(
'attachmentSaved'.tr(),
action: SnackBarAction(
label: 'openInAlbum'.tr(),
onPressed: () async => Gal.open(),
),
(!kIsWeb && (Platform.isIOS || Platform.isAndroid)) ? 'attachmentSaved'.tr() : 'attachmentSavedDesktop'.tr(),
action: (!kIsWeb && (Platform.isIOS || Platform.isAndroid))
? SnackBarAction(
label: 'openInAlbum'.tr(),
onPressed: () async => Gal.open(),
)
: null,
);
}
@ -252,6 +261,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
).padding(right: 8),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
@ -327,10 +337,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
'${item.size} Bytes',
style: metaTextStyle,
),
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),

View File

@ -1,4 +1,4 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
@ -130,7 +130,7 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
Text('callCamera').tr(),
Switch(
value: call.enableVideo,
onChanged: (value) => call.setEnableAudio(value),
onChanged: call.setEnableVideo,
),
],
).padding(bottom: 5),

View File

@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/link_preview.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:swipe_to/swipe_to.dart';
@ -22,6 +23,7 @@ class ChatMessage extends StatelessWidget {
final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
const ChatMessage({
super.key,
required this.data,
@ -63,7 +65,7 @@ class ChatMessage extends StatelessWidget {
onReply!(data);
},
),
if (isOwner && onEdit != null)
if (isOwner && data.type == 'messages.new' && onEdit != null)
MenuItem(
label: 'edit'.tr(),
icon: Symbols.edit,
@ -71,7 +73,7 @@ class ChatMessage extends StatelessWidget {
onEdit!(data);
},
),
if (isOwner && onDelete != null)
if (isOwner && data.type == 'messages.new' && onDelete != null)
MenuItem(
label: 'delete'.tr(),
icon: Symbols.delete,
@ -109,9 +111,7 @@ class ChatMessage extends StatelessWidget {
radius: 12,
).padding(right: 6),
Text(
(data.sender.nick?.isNotEmpty ?? false)
? data.sender.nick!
: user?.nick ?? 'unknown',
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(6),
Text(
@ -123,8 +123,7 @@ class ChatMessage extends StatelessWidget {
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
@ -153,6 +152,8 @@ class ChatMessage extends StatelessWidget {
)
],
).opacity(isPending ? 0.5 : 1),
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
LinkPreviewWidget(text: data.body['text']!),
if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList(
data: data.preload!.attachments!,
@ -161,10 +162,7 @@ class ChatMessage extends StatelessWidget {
maxHeight: 520,
listPadding: const EdgeInsets.only(top: 8),
),
if (!hasMerged && !isCompact)
const Gap(12)
else if (!isCompact)
const Gap(6),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
],
),
),
@ -174,6 +172,7 @@ class ChatMessage extends StatelessWidget {
class _ChatMessageText extends StatelessWidget {
final SnChatMessage data;
const _ChatMessageText({super.key, required this.data});
@override
@ -184,6 +183,7 @@ class _ChatMessageText extends StatelessWidget {
children: [
MarkdownTextContent(
content: data.body['text'],
isSelectable: true,
isAutoWarp: true,
),
if (data.updatedAt != data.createdAt)
@ -212,6 +212,7 @@ class _ChatMessageText extends StatelessWidget {
class _ChatMessageSystemNotify extends StatelessWidget {
final SnChatMessage data;
const _ChatMessageSystemNotify({super.key, required this.data});
String _formatDuration(Duration duration) {

View File

@ -12,13 +12,12 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart';
import '../../providers/user_directory.dart';
class ChatMessageInput extends StatefulWidget {
final ChatMessageController controller;
final SnChannelMember? otherMember;
@ -79,7 +78,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final place = await attach.chunkedUploadInitialize(
(await media.length())!,
media.name,
'interactive',
'messaging',
null,
mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
);
@ -105,11 +104,6 @@ class ChatMessageInputState extends State<ChatMessageInput> {
return;
}
attach.putCache(
_attachments.where((e) => e.attachment != null).map((e) => e.attachment!),
noCheck: true,
);
// Send the message
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
widget.controller.sendMessage(

View File

@ -99,7 +99,7 @@ extension AppPromptExtension on BuildContext {
if (exception.response != null) {
content = Text(
'$preview\n\n(${exception.response?.statusCode}) ${exception.response?.data}',
'$preview\n\n${exception.requestOptions.uri.path}\n(${exception.response?.statusCode}) ${exception.response?.data}',
);
} else {
content = Text(preview);

View File

@ -0,0 +1,170 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/link.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../providers/link_preview.dart';
class LinkPreviewWidget extends StatefulWidget {
final String text;
const LinkPreviewWidget({super.key, required this.text});
@override
State<LinkPreviewWidget> createState() => _LinkPreviewWidgetState();
}
class _LinkPreviewWidgetState extends State<LinkPreviewWidget> {
final List<SnLinkMeta> _links = List.empty(growable: true);
Future<void> _getLinkMeta() async {
final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*');
final links = linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet();
final lp = context.read<SnLinkPreviewProvider>();
final List<Future<SnLinkMeta?>> futures = links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList();
final results = await Future.wait(futures);
_links.addAll(results.where((e) => e != null).map((e) => e!).toList());
if (_links.isNotEmpty && mounted) setState(() {});
}
@override
void initState() {
super.initState();
_getLinkMeta();
}
@override
Widget build(BuildContext context) {
if (_links.isEmpty) return const SizedBox.shrink();
return Wrap(
spacing: 8,
runSpacing: 8,
children: _links.map((e) => _LinkPreviewEntry(meta: e)).toList(),
);
}
}
class _LinkPreviewEntry extends StatelessWidget {
final SnLinkMeta meta;
const _LinkPreviewEntry({
super.key,
required this.meta,
});
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? double.infinity : 480,
),
child: GestureDetector(
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (meta.image != null)
Container(
margin: const EdgeInsets.only(bottom: 4),
color: Theme.of(context).colorScheme.surfaceContainer,
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
child: AutoResizeUniversalImage(
meta.image!,
fit: BoxFit.contain,
),
),
),
),
SizedBox(
height: 48,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (meta.icon?.isNotEmpty ?? false)
StyledWidget(
meta.icon!.endsWith('.svg')
? SvgPicture.network(meta.icon!)
: UniversalImage(
meta.icon!,
width: 36,
height: 36,
cacheHeight: 36,
cacheWidth: 36,
),
).padding(all: 4, right: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 24,
child: ((meta.title?.length ?? 0) > 32)
? Marquee(
text: meta.title ?? 'unknown'.tr(),
style: TextStyle(fontSize: 17),
scrollAxis: Axis.horizontal,
showFadingOnlyWhenScrolling: true,
pauseAfterRound: const Duration(seconds: 3),
)
: Text(
meta.title ?? 'unknown'.tr(),
style: TextStyle(fontSize: 17),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (meta.siteName != null)
Text(
meta.siteName!,
style: TextStyle(fontSize: 13, height: 0.9),
).fontSize(11),
],
),
),
const Gap(6),
],
).padding(horizontal: 16),
),
if (meta.description != null)
Text(
meta.description!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
).padding(horizontal: 16, bottom: 8),
Text(
meta.url,
style: GoogleFonts.roboto(fontSize: 11, height: 0.9),
maxLines: 1,
overflow: TextOverflow.ellipsis,
).opacity(0.75).padding(horizontal: 16),
const Gap(4),
Text(
'poweredBy'.tr(args: ['HyperNet.Reader']),
style: GoogleFonts.roboto(fontSize: 11, height: 0.9),
).opacity(0.75).padding(horizontal: 16),
const Gap(16),
],
),
),
onTap: () {
launchUrlString(meta.url, mode: LaunchMode.externalApplication);
},
),
);
}
}

View File

@ -7,12 +7,11 @@ import 'package:responsive_framework/responsive_framework.dart';
class AppBackground extends StatelessWidget {
final Widget child;
final bool isLessOptimization;
final bool isRoot;
const AppBackground({
super.key,
required this.child,
this.isLessOptimization = false,
this.isRoot = false,
});
@ -23,52 +22,25 @@ class AppBackground extends StatelessWidget {
) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
if (isLessOptimization) {
final size = MediaQuery.of(context).size;
return Container(
color: Theme.of(context).colorScheme.surface,
child: Container(
decoration: BoxDecoration(
backgroundBlendMode: BlendMode.darken,
color: Theme.of(context).colorScheme.surface,
image: DecorationImage(
opacity: 0.2,
image: ResizeImage(
FileImage(imageFile),
width: (size.width * devicePixelRatio).round(),
height: (size.height * devicePixelRatio).round(),
policy: ResizeImagePolicy.fit,
),
fit: BoxFit.cover,
),
),
child: child,
),
);
}
final size = MediaQuery.of(context).size;
return Container(
color: Theme.of(context).colorScheme.surface,
child: LayoutBuilder(
builder: (context, constraints) {
return Container(
decoration: BoxDecoration(
backgroundBlendMode: BlendMode.darken,
color: Theme.of(context).colorScheme.surface,
image: DecorationImage(
opacity: 0.2,
image: ResizeImage(
FileImage(imageFile),
width: (constraints.maxWidth * devicePixelRatio).round(),
height: (constraints.maxHeight * devicePixelRatio).round(),
policy: ResizeImagePolicy.fit,
),
fit: BoxFit.cover,
),
child: Container(
decoration: BoxDecoration(
backgroundBlendMode: BlendMode.darken,
color: Theme.of(context).colorScheme.surface,
image: DecorationImage(
opacity: 0.2,
image: ResizeImage(
FileImage(imageFile),
width: (size.width * devicePixelRatio).round(),
height: (size.height * devicePixelRatio).round(),
policy: ResizeImagePolicy.fit,
),
child: child,
);
},
fit: BoxFit.cover,
),
),
child: child,
),
);
}
@ -77,11 +49,9 @@ class AppBackground extends StatelessWidget {
Widget build(BuildContext context) {
return ScaffoldMessenger(
child: FutureBuilder(
future:
kIsWeb ? Future.value(null) : getApplicationDocumentsDirectory(),
future: kIsWeb ? Future.value(null) : getApplicationDocumentsDirectory(),
builder: (context, snapshot) {
if (isRoot ||
ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) {
if (isRoot || ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) {
if (snapshot.hasData) {
final path = '${snapshot.data!.path}/app_background_image';
final file = File(path);

View File

@ -1,11 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/version_label.dart';
class AppNavigationDrawer extends StatefulWidget {
final double? elevation;
@ -49,19 +49,11 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
FutureBuilder<String>(
future: PackageInfo.fromPlatform().then((value) => 'Stable ${value.version}+${value.buildNumber}'),
builder: (context, snapshot) {
return Text(!snapshot.hasData ? 'Stable 2.0' : snapshot.data!)
.fontSize(12)
.textColor(Theme.of(context).colorScheme.onSurface.withOpacity(0.5));
},
),
AppVersionLabel(),
],
).padding(
horizontal: 32,
top: MediaQuery.of(context).padding.top + 12,
bottom: 12,
vertical: 12,
),
...destinations.where((ele) => ele.isPinned).map((ele) {
return NavigationDrawerDestination(

View File

@ -118,7 +118,7 @@ class AppRootScaffold extends StatelessWidget {
WindowTitleBarBox(
child: MoveWindow(
child: Text(
'Solian',
'Solar Network',
style: GoogleFonts.spaceGrotesk(),
).padding(horizontal: 12, vertical: 5),
),

View File

@ -2,13 +2,19 @@ import 'dart:io';
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:path_provider/path_provider.dart';
import 'package:popover/popover.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
@ -18,6 +24,7 @@ import 'package:surface/types/reaction.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/link_preview.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:gap/gap.dart';
import 'package:surface/widgets/post/post_comment_list.dart';
@ -52,10 +59,70 @@ class PostItem extends StatelessWidget {
if (onChanged != null) onChanged!(data);
}
void _doShare(BuildContext context) {
final box = context.findRenderObject() as RenderBox?;
final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
} else {
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
}
}
void _doShareViaPicture(BuildContext context) async {
final box = context.findRenderObject() as RenderBox?;
context.showSnackbar('postSharingViaPicture'.tr());
final controller = ScreenshotController();
final capturedImage = await controller.captureFromLongWidget(
InheritedTheme.captureAll(
context,
MediaQuery(
data: MediaQuery.of(context),
child: Material(
child: MultiProvider(
providers: [
Provider<SnNetworkProvider>(create: (_) => context.read()),
],
child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
child: PostShareImageWidget(data: data),
),
),
),
),
),
pixelRatio: 3,
context: context,
);
if (kIsWeb) return;
final directory = await getTemporaryDirectory();
final imageFile = await File(
'${directory.path}/sn-share-via-image-${DateTime.now().millisecondsSinceEpoch}.png',
).create();
await imageFile.writeAsBytes(capturedImage);
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
await Share.shareXFiles(
[XFile(imageFile.path)],
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}', file: imageFile);
}
await imageFile.delete();
}
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
// Article headline preview
if (!showFullPost && data.type == 'article') {
return Container(
@ -65,6 +132,10 @@ class PostItem extends StatelessWidget {
children: [
_PostContentHeader(
data: data,
isAuthor: isAuthor,
isRelativeDate: !showFullPost,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onDeleted: () {
if (onDeleted != null) {}
},
@ -116,6 +187,8 @@ class PostItem extends StatelessWidget {
data: data,
showComments: showComments,
showReactions: showReactions,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onChanged: _onChanged,
).padding(left: 8, right: 14),
],
@ -132,8 +205,12 @@ class PostItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_PostContentHeader(
isAuthor: isAuthor,
isRelativeDate: !showFullPost,
data: data,
showMenu: showMenu,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onDeleted: () {
if (onDeleted != null) onDeleted!();
},
@ -143,10 +220,12 @@ class PostItem extends StatelessWidget {
data: data,
isEnlarge: data.type == 'article' && showFullPost,
).padding(horizontal: 16, bottom: 8),
_PostContentBody(
data: data,
isEnlarge: data.type == 'article' && showFullPost,
).padding(horizontal: 16, bottom: 6),
if (data.body['content']?.isNotEmpty ?? false)
_PostContentBody(
data: data,
isSelectable: showFullPost,
isEnlarge: data.type == 'article' && showFullPost,
).padding(horizontal: 16, bottom: 6),
if (data.repostTo != null)
_PostQuoteContent(child: data.repostTo!).padding(
horizontal: 12,
@ -173,6 +252,10 @@ class PostItem extends StatelessWidget {
maxHeight: 560,
listPadding: const EdgeInsets.symmetric(horizontal: 12),
),
if (data.body['content'] != null)
LinkPreviewWidget(
text: data.body['content'],
).padding(horizontal: 4),
Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column(
@ -181,6 +264,8 @@ class PostItem extends StatelessWidget {
data: data,
showComments: showComments,
showReactions: showReactions,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onChanged: _onChanged,
).padding(left: 8, right: 14),
],
@ -191,28 +276,173 @@ class PostItem extends StatelessWidget {
}
}
class PostShareImageWidget extends StatelessWidget {
const PostShareImageWidget({
super.key,
required this.data,
});
final SnPost data;
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return SizedBox(
width: 480,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (data.preload?.thumbnail != null)
AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.preload!.thumbnail!.rid),
fit: BoxFit.cover,
),
),
).padding(bottom: 8),
_PostContentHeader(
isAuthor: false,
data: data,
onDeleted: () {},
onShare: () {},
onShareImage: () {},
showMenu: false,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
_PostHeadline(
data: data,
isEnlarge: data.type == 'article',
).padding(horizontal: 16, bottom: 8),
if (data.body['content']?.isNotEmpty ?? false)
_PostContentBody(
data: data,
isEnlarge: data.type == 'article',
).padding(horizontal: 16, bottom: 8),
if (data.repostTo != null)
_PostQuoteContent(
child: data.repostTo!,
isRelativeDate: false,
isFlatted: true,
).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
AttachmentList(
data: data.preload!.attachments!,
isFlatted: true,
).padding(horizontal: 16, bottom: 8),
if (data.body['content'] != null)
LinkPreviewWidget(
text: data.body['content'],
).padding(horizontal: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data.visibility > 0) _PostVisibilityHint(data: data),
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data),
],
).padding(horizontal: 16),
_PostBottomAction(
data: data,
showComments: true,
showReactions: true,
onShare: () {},
onShareImage: () {},
onChanged: (SnPost data) {},
).padding(left: 8, right: 14),
const Divider(height: 1),
const Gap(12),
SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${data.aliasPrefix} / ${data.alias ?? '#${data.id}'}',
style: GoogleFonts.robotoMono(fontSize: 17),
),
const Gap(2),
Text(
switch (data.type) {
'article' => 'postArticle'.tr(),
_ => 'postStory'.tr(),
},
style: GoogleFonts.robotoMono(fontSize: 12),
),
],
),
),
if (data.body['content_truncated'] == true)
Text(
'postImageShareReadMore'.tr(),
style: GoogleFonts.robotoMono(fontSize: 11),
),
Text(
'postImageShareAds',
style: GoogleFonts.robotoMono(fontSize: 13),
).tr(),
],
),
),
QrImageView(
padding: EdgeInsets.zero,
data: 'https://solsynth.dev/posts/${data.id}',
errorCorrectionLevel: QrErrorCorrectLevel.H,
version: QrVersions.auto,
size: 100,
gapless: true,
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(28, 28),
),
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
)
],
),
).padding(left: 16, right: 32, vertical: 8),
],
).padding(vertical: 16),
);
}
}
class _PostBottomAction extends StatelessWidget {
final SnPost data;
final bool showComments;
final bool showReactions;
final Function(SnPost data) onChanged;
final Function() onShare, onShareImage;
const _PostBottomAction({
required this.data,
required this.showComments,
required this.showReactions,
required this.onChanged,
required this.onShare,
required this.onShareImage,
});
void _doShare() {
final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url));
} else {
Share.share(url);
}
}
@override
Widget build(BuildContext context) {
final iconColor = Theme.of(context).colorScheme.onSurface.withAlpha(
@ -228,6 +458,7 @@ class _PostBottomAction extends StatelessWidget {
children: [
if (showReactions || showComments)
Row(
spacing: 8,
children: [
if (showReactions)
InkWell(
@ -293,11 +524,11 @@ class _PostBottomAction extends StatelessWidget {
);
},
),
].expand((ele) => [ele, const Gap(8)]).toList()
..removeLast(),
],
),
InkWell(
onTap: _doShare,
onTap: onShare,
onLongPress: onShareImage,
child: Icon(
Symbols.share,
size: 20,
@ -369,10 +600,10 @@ class _PostHeadline extends StatelessWidget {
style: TextStyle(fontSize: 13),
),
const Gap(8),
if (data.updatedAt != data.createdAt)
if (data.editedAt != null)
Text(
'articleEditedAt'.tr(
args: [DateFormat('y/M/d HH:mm').format(data.updatedAt)],
args: [DateFormat('y/M/d HH:mm').format(data.editedAt!)],
),
style: TextStyle(fontSize: 13),
),
@ -405,15 +636,22 @@ class _PostHeadline extends StatelessWidget {
class _PostContentHeader extends StatelessWidget {
final SnPost data;
final bool isAuthor;
final bool isCompact;
final bool isRelativeDate;
final bool showMenu;
final Function onDeleted;
final Function() onShare, onShareImage;
const _PostContentHeader({
required this.data,
required this.isAuthor,
this.isCompact = false,
this.isRelativeDate = true,
this.showMenu = true,
required this.onDeleted,
required this.onShare,
required this.onShareImage,
});
Future<void> _deletePost(BuildContext context) async {
@ -441,9 +679,6 @@ class _PostContentHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user!.id;
return Row(
children: [
GestureDetector(
@ -479,9 +714,11 @@ class _PostContentHeader extends StatelessWidget {
children: [
Text('@${data.publisher.name}').fontSize(13),
const Gap(4),
Text(RelativeTime(context).format(
data.publishedAt ?? data.createdAt,
)).fontSize(13),
Text(
isRelativeDate
? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
).fontSize(13),
],
).opacity(0.8),
],
@ -496,9 +733,11 @@ class _PostContentHeader extends StatelessWidget {
children: [
Text('@${data.publisher.name}').fontSize(13),
const Gap(4),
Text(RelativeTime(context).format(
data.publishedAt ?? data.createdAt,
)).fontSize(13),
Text(
isRelativeDate
? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
).fontSize(13),
],
).opacity(0.8),
],
@ -573,6 +812,27 @@ class _PostContentHeader extends StatelessWidget {
},
),
const PopupMenuDivider(),
PopupMenuItem(
onTap: onShare,
child: Row(
children: [
const Icon(Symbols.share),
const Gap(16),
Text('postShare').tr(),
],
),
),
PopupMenuItem(
onTap: onShareImage,
child: Row(
children: [
const Icon(Symbols.share_reviews),
const Gap(16),
Text('postShareImage').tr(),
],
),
),
const PopupMenuDivider(),
PopupMenuItem(
child: Row(
children: [
@ -604,16 +864,19 @@ class _PostContentHeader extends StatelessWidget {
class _PostContentBody extends StatelessWidget {
final SnPost data;
final bool isEnlarge;
final bool isSelectable;
const _PostContentBody({
required this.data,
this.isEnlarge = false,
this.isSelectable = false,
});
@override
Widget build(BuildContext context) {
if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent(
isSelectable: isSelectable,
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
attachments: data.preload?.attachments,
@ -623,8 +886,15 @@ class _PostContentBody extends StatelessWidget {
class _PostQuoteContent extends StatelessWidget {
final SnPost child;
final bool isRelativeDate;
final bool isFlatted;
const _PostQuoteContent({super.key, required this.child});
const _PostQuoteContent({
super.key,
this.isRelativeDate = true,
this.isFlatted = false,
required this.child,
});
@override
Widget build(BuildContext context) {
@ -637,16 +907,42 @@ class _PostQuoteContent extends StatelessWidget {
width: 1,
),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.only(top: 8),
child: Column(
children: [
_PostContentHeader(
data: child,
isCompact: true,
showMenu: false,
onDeleted: () {},
).padding(bottom: 4),
_PostContentBody(data: child),
Column(
children: [
_PostContentHeader(
isAuthor: false,
data: child,
isCompact: true,
isRelativeDate: isRelativeDate,
showMenu: false,
onShare: () {},
onShareImage: () {},
onDeleted: () {},
).padding(bottom: 4),
_PostContentBody(data: child),
if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4),
],
).padding(horizontal: 16),
if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
child: AttachmentList(
data: child.preload!.attachments!,
isFlatted: isFlatted,
listPadding: const EdgeInsets.symmetric(horizontal: 12),
),
).padding(
top: 8,
bottom: (child.preload?.attachments?.length ?? 0) > 1 ? 12 : 0,
)
else
const Gap(8),
],
),
),
@ -720,32 +1016,35 @@ class _PostTruncatedHint extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
if (data.body['content_length'] != null)
Row(
children: [
const Icon(Symbols.timer, size: 20),
const Gap(4),
Text('postReadEstimate').tr(args: [
'${Duration(
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
).inSeconds}s',
]),
],
).padding(right: 8),
if (data.body['content_length'] != null)
Row(
children: [
const Icon(Symbols.height, size: 20),
const Gap(4),
Text(
'postTotalLength'.plural(data.body['content_length']),
)
],
),
],
).opacity(0.75);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
if (data.body['content_length'] != null)
Row(
children: [
const Icon(Symbols.timer, size: 20),
const Gap(4),
Text('postReadEstimate').tr(args: [
'${Duration(
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
).inSeconds}s',
]),
],
).padding(right: 8),
if (data.body['content_length'] != null)
Row(
children: [
const Icon(Symbols.height, size: 20),
const Gap(4),
Text(
'postTotalLength'.plural(data.body['content_length']),
)
],
),
],
).opacity(0.75),
);
}
}

View File

@ -35,7 +35,7 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers');
final resp = await sn.client.get('/cgi/co/publishers/me');
_publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
);

View File

@ -1,12 +1,15 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart';
// Keep this import to make the web image render work
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:surface/providers/config.dart';
class UniversalImage extends StatelessWidget {
final String url;
final double? width, height;
@ -14,6 +17,7 @@ class UniversalImage extends StatelessWidget {
final bool noProgressIndicator;
final bool noErrorWidget;
final double? cacheWidth, cacheHeight;
final FilterQuality? filterQuality;
const UniversalImage(
this.url, {
@ -25,45 +29,43 @@ class UniversalImage extends StatelessWidget {
this.noErrorWidget = false,
this.cacheWidth,
this.cacheHeight,
this.filterQuality,
});
@override
Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final double? resizeHeight =
cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
final double? resizeWidth =
cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
return Image(
image: ResizeImage(
UniversalImage.provider(url),
width: resizeWidth?.round(),
height: resizeHeight?.round(),
policy: ResizeImagePolicy.fit,
),
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality,
image: kIsWeb
? UniversalImage.provider(url)
: ResizeImage(
UniversalImage.provider(url),
width: resizeWidth?.round(),
height: resizeHeight?.round(),
policy: ResizeImagePolicy.fit,
),
width: width,
height: height,
fit: fit,
loadingBuilder: noProgressIndicator
? null
: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: 0,
),
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? value.toDouble()
: null,
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
),
),
);
@ -94,10 +96,13 @@ class UniversalImage extends StatelessWidget {
}
static ImageProvider provider(String url) {
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
return CachedNetworkImageProvider(url);
}
return NetworkImage(url);
// This place used to use network image or cached network image depending on the platform.
// But now the cached network image is working on every platform.
// So we just use it now.
return CachedNetworkImageProvider(
url,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
);
}
}

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart';
class AppVersionLabel extends StatelessWidget {
final double fontSize;
const AppVersionLabel({super.key, this.fontSize = 12});
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: PackageInfo.fromPlatform().then((value) => 'Stable ${value.version}+${value.buildNumber}'),
builder: (context, snapshot) {
return Text(!snapshot.hasData ? 'Stable 2.0' : snapshot.data!).fontSize(fontSize).textColor(
Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
);
},
);
}
}

View File

@ -7,19 +7,22 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_udid/flutter_udid_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
#include <sentry_flutter/sentry_flutter_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
g_autoptr(FlPluginRegistrar) file_saver_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
file_saver_plugin_register_with_registrar(file_saver_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
@ -38,9 +41,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) sentry_flutter_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin");
sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -4,19 +4,18 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_linux
file_saver
file_selector_linux
flutter_udid
flutter_webrtc
media_kit_libs_linux
media_kit_video
pasteboard
sentry_flutter
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
croppy
jni
media_kit_native_event_loop
)

View File

@ -8,6 +8,7 @@ import Foundation
import bitsdojo_window_macos
import connectivity_plus
import device_info_plus
import file_saver
import file_selector_macos
import firebase_analytics
import firebase_core
@ -15,6 +16,7 @@ import firebase_messaging
import flutter_udid
import flutter_webrtc
import gal
import in_app_review
import livekit_client
import media_kit_libs_macos_video
import media_kit_video
@ -22,7 +24,6 @@ import package_info_plus
import pasteboard
import path_provider_foundation
import screen_brightness_macos
import sentry_flutter
import share_plus
import shared_preferences_foundation
import sqflite_darwin
@ -33,6 +34,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
@ -40,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
@ -47,7 +50,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@ -6,10 +6,10 @@ PODS:
- FlutterMacOS
- croppy (0.0.1):
- FlutterMacOS
- cupertino_http (0.0.1):
- FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- file_saver (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/Analytics (11.4.0):
@ -26,7 +26,7 @@ PODS:
- Firebase/Analytics (= 11.4.0)
- firebase_core
- FlutterMacOS
- firebase_core (3.8.1):
- firebase_core (3.9.0):
- Firebase/CoreOnly (~> 11.4.0)
- FlutterMacOS
- firebase_messaging (15.1.6):
@ -56,7 +56,7 @@ PODS:
- FirebaseCoreInternal (~> 11.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)
@ -132,7 +132,10 @@ PODS:
- GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- livekit_client (2.3.1):
- in_app_review (2.0.0):
- FlutterMacOS
- livekit_client (2.3.2):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 125.6422.06)
- media_kit_libs_macos_video (1.0.4):
@ -157,11 +160,6 @@ PODS:
- SAMKeychain (1.5.3)
- screen_brightness_macos (0.1.0):
- FlutterMacOS
- Sentry/HybridSDK (8.40.1)
- sentry_flutter (8.10.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.40.1)
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
@ -180,8 +178,8 @@ DEPENDENCIES:
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
- cupertino_http (from `Flutter/ephemeral/.symlinks/plugins/cupertino_http/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
@ -190,6 +188,7 @@ DEPENDENCIES:
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
@ -198,7 +197,6 @@ DEPENDENCIES:
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
@ -219,7 +217,6 @@ SPEC REPOS:
- nanopb
- PromisesObjC
- SAMKeychain
- Sentry
- WebRTC-SDK
EXTERNAL SOURCES:
@ -229,10 +226,10 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
croppy:
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
cupertino_http:
:path: Flutter/ephemeral/.symlinks/plugins/cupertino_http/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_saver:
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
firebase_analytics:
@ -249,6 +246,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
gal:
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
in_app_review:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
livekit_client:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
media_kit_libs_macos_video:
@ -265,8 +264,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_brightness_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
sentry_flutter:
:path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
@ -280,28 +277,29 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
croppy: 25a638bd7d05411d8c697f481568f261037694fc
cupertino_http: 1d506209e339211efa0764caa7b2de4bc55b6818
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef
firebase_core: e4a35c426636a2cce00a5163df7ba69bfd0cca57
firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f
firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
flutter_udid: 6b2b89780c3dfeecf0047bdf93f622d6416b1c07
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
livekit_client: 8838dc52cb95b452968efb84e87cad895c261d9d
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: 9fdcb22df3de55e6d4b24bdc3b5eb1c0269d774a
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
@ -312,8 +310,6 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
Sentry: e9215d7b17f7902692b4f8700e061e4f853e3521
sentry_flutter: 927eed60d66951d1b0f1db37fe94ff5cb7c80231
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d

View File

@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View File

@ -10,8 +10,6 @@
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>

View File

@ -31,16 +31,18 @@
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Photo Library will allow Solian take photo or video for your post.</string>
<string>Grant access to Camera will allow Solian use your camera during a call.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Photo Library will allow Solian record audio for your post.</string>
<string>Grant access to Microphone will allow Solian use your microphone during a call.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>NSCameraUseContinuityCameraDeviceType</key>
<string></string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
</dict>
</plist>

View File

@ -8,8 +8,6 @@
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>

Some files were not shown because too many files have changed in this diff Show More