Compare commits

...

54 Commits

Author SHA1 Message Date
LittleSheep
fbbab0a981 Send delete session requset when logout 2025-08-14 02:29:32 +08:00
LittleSheep
ae2fb3b303 👽 Support new authorized device 2025-08-14 02:10:21 +08:00
LittleSheep
3d7a4666ed 👔 Skip the debug mode crashlytics setup 2025-08-13 15:32:34 +08:00
LittleSheep
5d3e0fb800 🐛 Fix gha artificats has same name 2025-08-13 15:26:00 +08:00
LittleSheep
85ff52a661 🔨 Update windows setup build script and use gha to do so 2025-08-13 13:55:11 +08:00
LittleSheep
da7fd64a43 Merge branch 'v3' of https://github.com/Solsynth/Solian into v3 2025-08-13 13:40:58 +08:00
LittleSheep
3902633217 🔨 Update windows setup builder script 2025-08-13 13:40:53 +08:00
LittleSheep
f478ea8b84 🐛 Serval bug fixes 2025-08-13 13:06:20 +08:00
LittleSheep
0f481aff5b ♻️ Extract the poll related words 2025-08-13 00:35:44 +08:00
LittleSheep
7a31663310 🍱 Update translation 2025-08-12 22:55:20 +08:00
LittleSheep
0239c53c04 💄 Optimize poll submitted view 2025-08-12 22:52:52 +08:00
LittleSheep
16987c758e 💄 Optimize poll 2025-08-12 22:52:05 +08:00
LittleSheep
3a36915140 Setup windows installer 2025-08-12 15:25:41 +08:00
LittleSheep
4bde708878 🐛 Fix windows 2025-08-12 13:04:15 +08:00
LittleSheep
2f0cf560f8 🐛 Hide share via screenshot on web 2025-08-11 22:23:21 +08:00
LittleSheep
cf355a95fd Post share card 2025-08-11 22:18:35 +08:00
LittleSheep
2f43073172 🐛 Hide actions on user himself's profile page 2025-08-11 22:02:50 +08:00
LittleSheep
8236d31ecc ♻️ Refactor the two types of post item 2025-08-11 21:33:10 +08:00
LittleSheep
459a7dade0 🚀 Launch 3.2.0+124 2025-08-11 18:56:32 +08:00
LittleSheep
e6000a660a 📈 Add firebase analytics 2025-08-11 17:59:05 +08:00
LittleSheep
75abaac205 📈 Setup firebase crash handler 2025-08-11 17:25:31 +08:00
LittleSheep
603d5c3f73 Remove keyboard nav 2025-08-11 16:46:43 +08:00
LittleSheep
4e4bd99598 🚀 Launch 3.1.0+123 2025-08-11 02:06:29 +08:00
LittleSheep
d1fbe5f15e 🐛 Dozens of bug fixes 2025-08-11 01:56:19 +08:00
LittleSheep
c061ef2132 🐛 Bug fixes 2025-08-11 01:44:18 +08:00
LittleSheep
c378309bdd 📝 Update localization 2025-08-11 01:44:12 +08:00
LittleSheep
b2c5d64fc5 Keyboard navigation basis 2025-08-10 16:57:11 +08:00
LittleSheep
5371637b16 🔀 Merge pull request #161 from Texas0295/v3
🐛 linux: guard FirebaseMessaging on unsupported platforms
2025-08-10 14:05:48 +08:00
LittleSheep
c5cbf0af37 ⬆️ Upgrade android native project 2025-08-10 13:51:19 +08:00
LittleSheep
1a31e22450 🐛 Fix stickers pack unable to create 2025-08-10 13:23:15 +08:00
Texas0295
49db54529d 🐛 linux: guard FirebaseMessaging on unsupported platforms 2025-08-10 13:17:48 +08:00
LittleSheep
8e0c0c6054 🚀 Launch 3.1.0+122 2025-08-10 04:16:58 +08:00
LittleSheep
f3d1183076 🐛 Fix android update 2025-08-10 04:16:53 +08:00
LittleSheep
a9f7f0cce0 🐛 Fix bugs, ah bugs ha ha, bugs 2025-08-10 04:04:31 +08:00
LittleSheep
f2943f8411 🐛 Fix iOS notify delegate wrong path 2025-08-10 03:30:57 +08:00
LittleSheep
808e7dcffa Option to boost github assets download 2025-08-10 03:25:57 +08:00
LittleSheep
9bed4fa6fb Android install update 2025-08-10 03:21:34 +08:00
LittleSheep
e6255a340b 🐛 Fix background image didn't apply in certain page 2025-08-10 02:59:28 +08:00
LittleSheep
78bf319fb7 💄 Adjust stickers styles 2025-08-10 02:59:11 +08:00
LittleSheep
36a966d582 🐛 Fix profile link parsing 2025-08-10 02:55:00 +08:00
LittleSheep
f72b268d36 💄 Optimize profile page 2025-08-10 02:29:46 +08:00
LittleSheep
44ef31034e 👽 Update the profile links 2025-08-10 02:17:06 +08:00
LittleSheep
229dc2186f 💄 Optimize markdown rendering 2025-08-10 01:53:14 +08:00
LittleSheep
a2f9a1efb4 Optimize post quick reply 2025-08-10 01:45:02 +08:00
LittleSheep
823e3c5de6 🔀 Merge pull request #160 from Texas0295/v3
[FIX] linux_firebase_guard: skip FirebaseMessaging calls on Linux
2025-08-10 01:11:29 +08:00
Texas0295
faac7bac35 🐛 linux: guard FirebaseMessaging calls when Firebase is not initialized 2025-08-10 00:40:49 +08:00
LittleSheep
1fac1bfe02 🐛 Fix post item and payment 2025-08-10 00:26:32 +08:00
LittleSheep
9394b1d9c8 🐛 Serval bug fixes 2025-08-09 23:24:21 +08:00
LittleSheep
43dd13bac4 🐛 Fix status update issue 2025-08-09 22:55:46 +08:00
LittleSheep
65bc372103 🐛 Fix iOS NSE wrong avatar path 2025-08-09 22:55:35 +08:00
LittleSheep
6558854a7a 🚀 Launch 3.1.0+120 2025-08-09 13:37:35 +08:00
LittleSheep
892035ab27 🐛 Somehow fix production video player issue 2025-08-09 13:35:54 +08:00
LittleSheep
87ae8d2ff4 🚀 Launch 3.1.0+119 2025-08-09 01:44:18 +08:00
LittleSheep
15c2dbaa0d 🐛 Fix developer 2025-08-09 01:41:51 +08:00
98 changed files with 5937 additions and 3105 deletions

View File

@@ -41,6 +41,15 @@ jobs:
with: with:
name: build-output-windows name: build-output-windows
path: build/windows/x64/runner/Release path: build/windows/x64/runner/Release
- name: Compile Installer
uses: Minionguyjpro/Inno-Setup-Action@v1.2.2
with:
path: setup.iss
- name: Archive installer artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-windows-installer
path: Installer/windows-x86_64-setup.exe
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

3
.gitignore vendored
View File

@@ -12,6 +12,9 @@
.swiftpm/ .swiftpm/
migrate_working_dir/ migrate_working_dir/
# Inno Setup
Installer/
# IntelliJ related # IntelliJ related
*.iml *.iml
*.ipr *.ipr

View File

@@ -5,6 +5,7 @@ plugins {
id("com.android.application") id("com.android.application")
// START: FlutterFire Configuration // START: FlutterFire Configuration
id("com.google.gms.google-services") id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
// END: FlutterFire Configuration // END: FlutterFire Configuration
id("kotlin-android") id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
@@ -51,6 +52,12 @@ android {
buildTypes { buildTypes {
release { release {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
} }
@@ -58,7 +65,7 @@ android {
dependencies { dependencies {
implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.material:material:1.12.0")
implementation("com.github.bumptech.glide:glide:4.16.0") implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:okhttp:5.1.0")
} }
flutter { flutter {

5
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,5 @@
# JNI Zero initialization (required for WebRTC native method registration)
-keep class livekit.org.jni_zero.JniInit {
# Keep the init method un-obfuscated for native code callback
private static java.lang.Object[] init();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip

View File

@@ -18,11 +18,12 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.10.1" apply false id("com.android.application") version "8.12.0" apply false
// START: FlutterFire Configuration // START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false id("com.google.gms.google-services") version("4.3.15") apply false
id("com.google.firebase.crashlytics") version("2.8.1") apply false
// END: FlutterFire Configuration // END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "1.8.22" apply false id("org.jetbrains.kotlin.android") version("2.2.0") apply false
} }
include(":app") include(":app")

View File

@@ -573,6 +573,7 @@
"keyboardShortcuts": "Keyboard Shortcuts", "keyboardShortcuts": "Keyboard Shortcuts",
"share": "Share", "share": "Share",
"sharePost": "Share Post", "sharePost": "Share Post",
"sharePostPhoto": "Share Post as Photo",
"quickActions": "Quick Actions", "quickActions": "Quick Actions",
"post": "Post", "post": "Post",
"copy": "Copy", "copy": "Copy",
@@ -706,6 +707,7 @@
"copyToClipboardTooltip": "Copy to clipboard", "copyToClipboardTooltip": "Copy to clipboard",
"postForwardingTo": "Forwarding to", "postForwardingTo": "Forwarding to",
"postReplyingTo": "Replying to", "postReplyingTo": "Replying to",
"postReplyPlaceholder": "Post your reply",
"postEditing": "You are editing an existing post", "postEditing": "You are editing an existing post",
"postArticle": "Article", "postArticle": "Article",
"aboutDeviceName": "Device Name", "aboutDeviceName": "Device Name",
@@ -759,6 +761,7 @@
"pollsRecent": "Recent Polls", "pollsRecent": "Recent Polls",
"pollCreateNew": "Create New", "pollCreateNew": "Create New",
"pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.", "pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.",
"pollQuestions": "Questions",
"publisher": "Publisher", "publisher": "Publisher",
"publisherHint": "Enter the publisher name", "publisherHint": "Enter the publisher name",
"publisherCannotBeEmpty": "Publisher cannot be empty", "publisherCannotBeEmpty": "Publisher cannot be empty",
@@ -787,5 +790,51 @@
"addLink": "Add link", "addLink": "Add link",
"linkKey": "Link Name", "linkKey": "Link Name",
"linkValue": "URL", "linkValue": "URL",
"debugOptions": "Debug Options" "debugOptions": "Debug Options",
} "joinedAt": "Joined at {}",
"searchAccounts": "Search accounts...",
"webFeeds": "Web Feeds",
"polls": "Polls",
"sharePostSlogan": "Explore more on the Solar Network",
"filesListAdditional": {
"one": "+{} file remaining",
"other": "+{} files remaining"
},
"pollAnswerSubmitted": "Poll answer has been submitted.",
"modifyAnswers": "Modify Answers",
"back": "Back",
"submit": "Submit",
"pollOptionDefaultLabel": "Option 1",
"pollUpdated": "Poll updated.",
"pollCreated": "Poll created.",
"pollCreate": "Create Poll",
"pollEdit": "Edit Poll",
"pollPreviewJsonDebug": "Debug Preview",
"pollTitleRequired": "Title is required",
"pollEndDateOptional": "End date & time (optional)",
"notSet": "Not set",
"pick": "Pick",
"clear": "Clear",
"questions": "Questions",
"pollAddQuestion": "Add question",
"pollQuestionTypeSingleChoice": "Single choice",
"pollQuestionTypeMultipleChoice": "Multiple choice",
"pollQuestionTypeFreeText": "Free text",
"pollQuestionTypeYesNo": "Yes / No",
"pollQuestionTypeRating": "Rating",
"pollNoQuestionsYet": "No questions yet",
"pollNoQuestionsHint": "Use \"Add question\" to start building your poll.",
"pollDebugPreview": "Debug Preview",
"pollUntitledQuestion": "Untitled question",
"moveUp": "Move up",
"moveDown": "Move down",
"required": "Required",
"pollQuestionTitle": "Question title",
"pollQuestionTitleRequired": "Question title is required",
"pollQuestionDescriptionOptional": "Question description (optional)",
"options": "Options",
"pollAddOption": "Add option",
"pollOptionLabel": "Option label",
"pollLongTextAnswerPreview": "Long text answer (preview)",
"pollShortTextAnswerPreview": "Short text answer (preview)"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
assets/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -42,22 +42,62 @@ PODS:
- Flutter - Flutter
- Firebase/CoreOnly (12.0.0): - Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0) - FirebaseCore (~> 12.0.0)
- Firebase/Crashlytics (12.0.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.0.0)
- Firebase/Messaging (12.0.0): - Firebase/Messaging (12.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.0.0) - FirebaseMessaging (~> 12.0.0)
- firebase_analytics (12.0.0):
- firebase_core
- FirebaseAnalytics (= 12.0.0)
- Flutter
- firebase_core (4.0.0): - firebase_core (4.0.0):
- Firebase/CoreOnly (= 12.0.0) - Firebase/CoreOnly (= 12.0.0)
- Flutter - Flutter
- firebase_crashlytics (5.0.0):
- Firebase/Crashlytics (= 12.0.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.0): - firebase_messaging (16.0.0):
- Firebase/Messaging (= 12.0.0) - Firebase/Messaging (= 12.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseAnalytics (12.0.0):
- FirebaseAnalytics/Default (= 12.0.0)
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleAppMeasurement/Default (= 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.0.0): - FirebaseCore (12.0.0):
- FirebaseCoreInternal (~> 12.0.0) - FirebaseCoreInternal (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreInternal (12.0.0): - FirebaseCoreInternal (12.0.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseRemoteConfigInterop (~> 12.0.0)
- FirebaseSessions (~> 12.0.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (12.0.0): - FirebaseInstallations (12.0.0):
- FirebaseCore (~> 12.0.0) - FirebaseCore (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
@@ -72,7 +112,19 @@ PODS:
- GoogleUtilities/Reachability (~> 8.1) - GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (12.0.0)
- FirebaseSessions (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreExtension (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1): - flutter_inappwebview_ios (0.0.1):
- Flutter - Flutter
- flutter_inappwebview_ios/Core (= 0.0.1) - flutter_inappwebview_ios/Core (= 0.0.1)
@@ -99,6 +151,32 @@ PODS:
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAdsOnDeviceConversion (2.1.0):
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.0.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.0.0):
- GoogleAdsOnDeviceConversion (= 2.1.0)
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleAppMeasurement/IdentitySupport (= 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.0.0):
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0): - GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
@@ -112,6 +190,9 @@ PODS:
- GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment - GoogleUtilities/Environment
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0): - GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib" - "GoogleUtilities/NSData+zlib"
@@ -160,6 +241,8 @@ PODS:
- pointer_interceptor_ios (0.0.1): - pointer_interceptor_ios (0.0.1):
- Flutter - Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- receive_sharing_intent (1.8.1): - receive_sharing_intent (1.8.1):
- Flutter - Flutter
- record_ios (1.0.0): - record_ios (1.0.0):
@@ -178,25 +261,25 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.50.3): - sqlite3 (3.50.4):
- sqlite3/common (= 3.50.3) - sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.3) - sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.3): - sqlite3/dbstatvtab (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.50.3): - sqlite3/fts5 (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.50.3): - sqlite3/math (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.50.3): - sqlite3/perf-threadsafe (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.50.3): - sqlite3/rtree (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/session (3.50.3): - sqlite3/session (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.50.3) - sqlite3 (~> 3.50.4)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/math - sqlite3/math
@@ -220,9 +303,12 @@ DEPENDENCIES:
- croppy (from `.symlinks/plugins/croppy/ios`) - croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
@@ -262,16 +348,24 @@ SPEC REPOS:
- DKImagePickerController - DKImagePickerController
- DKPhotoGallery - DKPhotoGallery
- Firebase - Firebase
- FirebaseAnalytics
- FirebaseCore - FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal - FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations - FirebaseInstallations
- FirebaseMessaging - FirebaseMessaging
- FirebaseRemoteConfigInterop
- FirebaseSessions
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- Kingfisher - Kingfisher
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
- PromisesSwift
- SAMKeychain - SAMKeychain
- SDWebImage - SDWebImage
- sqlite3 - sqlite3
@@ -287,12 +381,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging: firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios" :path: ".symlinks/plugins/firebase_messaging/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios: flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios" :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility: flutter_keyboard_visibility:
@@ -365,13 +465,21 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4
firebase_crashlytics: 2c6c1a17900a38081d938330e9f48e60ec5b255d
firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361 firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361
FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7
FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a
FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361
FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55
FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613
FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
@@ -381,6 +489,8 @@ SPEC CHECKSUMS:
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457 flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457
gal: baecd024ebfd13c441269ca7404792a7152fde89 gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64
GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
@@ -398,6 +508,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
@@ -406,8 +517,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d url_launcher_ios: 694010445543906933d732453a59da0a173ae33d

View File

@@ -439,6 +439,7 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
8C0351B03869BBF493808288 /* [CP] Embed Pods Frameworks */, 8C0351B03869BBF493808288 /* [CP] Embed Pods Frameworks */,
5E7D6EF29B671AC7EDBA5649 /* [CP] Copy Pods Resources */, 5E7D6EF29B671AC7EDBA5649 /* [CP] Copy Pods Resources */,
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
); );
buildRules = ( buildRules = (
); );
@@ -682,6 +683,24 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
}; };
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n";
};
E947029FCA058878F9B63890 /* [CP] Check Pods Manifest.lock */ = { E947029FCA058878F9B63890 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;

View File

@@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
} }
let serverUrl = UserDefaults.standard.getServerUrl() let serverUrl = UserDefaults.standard.getServerUrl()
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages" let url = "\(serverUrl)/sphere/chat/\(metadata["room_id"] ?? "")/messages"
let parameters: [String: Any?] = [ let parameters: [String: Any?] = [
"content": textResponse.userText, "content": textResponse.userText,

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
func getAttachmentUrl(for identifier: String) -> String { func getAttachmentUrl(for identifier: String) -> String {
let serverBaseUrl = "https://nt.solian.app" let serverBaseUrl = "https://api.solian.app"
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/files/\(identifier)" return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
} }

View File

@@ -61,10 +61,8 @@ class DefaultFirebaseOptions {
messagingSenderId: '961776991058', messagingSenderId: '961776991058',
projectId: 'solian-0x001', projectId: 'solian-0x001',
storageBucket: 'solian-0x001.firebasestorage.app', storageBucket: 'solian-0x001.firebasestorage.app',
androidClientId: androidClientId: '961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com',
'961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com', iosClientId: '961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
iosClientId:
'961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
iosBundleId: 'dev.solsynth.solian', iosBundleId: 'dev.solsynth.solian',
); );
@@ -74,10 +72,8 @@ class DefaultFirebaseOptions {
messagingSenderId: '961776991058', messagingSenderId: '961776991058',
projectId: 'solian-0x001', projectId: 'solian-0x001',
storageBucket: 'solian-0x001.firebasestorage.app', storageBucket: 'solian-0x001.firebasestorage.app',
androidClientId: androidClientId: '961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com',
'961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com', iosClientId: '961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
iosClientId:
'961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
iosBundleId: 'dev.solsynth.solian', iosBundleId: 'dev.solsynth.solian',
); );
@@ -90,4 +86,5 @@ class DefaultFirebaseOptions {
storageBucket: 'solian-0x001.firebasestorage.app', storageBucket: 'solian-0x001.firebasestorage.app',
measurementId: 'G-JD1YEG9D6F', measurementId: 'G-JD1YEG9D6F',
); );
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:croppy/croppy.dart'; import 'package:croppy/croppy.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -30,7 +31,6 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface.
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
import 'package:island/services/update_service.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
@@ -62,6 +62,17 @@ void main() async {
FirebaseMessaging.onBackgroundMessage( FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler, _firebaseMessagingBackgroundHandler,
); );
// Although previous if case checked this. Still check is web or not
// Otherwise the web platform will broke due to there is no Platform api on the web
// Skip crashlytics setup on debug mode to prevent unexpected report to firebase
if ((kIsWeb || !Platform.isWindows) && !kDebugMode) {
FlutterError.onError =
FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
}
} }
log("[SplashScreen] Firebase is ready!"); log("[SplashScreen] Firebase is ready!");
@@ -144,15 +155,6 @@ void main() async {
), ),
), ),
); );
// Schedule update check shortly after startup, when a context is available.
// Uses the global overlay key to obtain a BuildContext safely.
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = globalOverlay.currentContext;
if (ctx != null) {
UpdateService().checkForUpdates(ctx);
}
});
} }
// Router will be provided through Riverpod // Router will be provided through Riverpod
@@ -181,6 +183,9 @@ class IslandApp extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
if (!kIsWeb && Platform.isLinux) {
return null;
}
const channel = MethodChannel('dev.solsynth.solian/notifications'); const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async { Future<void> handleInitialLink() async {

View File

@@ -1,9 +1,10 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/auth.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/wallet.dart'; import 'package:island/models/wallet.dart';
part 'user.freezed.dart'; part 'account.freezed.dart';
part 'user.g.dart'; part 'account.g.dart';
@freezed @freezed
sealed class SnAccount with _$SnAccount { sealed class SnAccount with _$SnAccount {
@@ -25,6 +26,32 @@ sealed class SnAccount with _$SnAccount {
_$SnAccountFromJson(json); _$SnAccountFromJson(json);
} }
@freezed
sealed class ProfileLink with _$ProfileLink {
const factory ProfileLink({required String name, required String url}) =
_ProfileLink;
factory ProfileLink.fromJson(Map<String, dynamic> json) =>
_$ProfileLinkFromJson(json);
}
class ProfileLinkConverter
implements JsonConverter<List<ProfileLink>, dynamic> {
const ProfileLinkConverter();
@override
List<ProfileLink> fromJson(dynamic json) {
return json is List<dynamic>
? json.map((e) => ProfileLink.fromJson(e)).cast<ProfileLink>().toList()
: <ProfileLink>[];
}
@override
List<dynamic> toJson(List<ProfileLink> object) {
return object.map((e) => e.toJson()).toList();
}
}
@freezed @freezed
sealed class SnAccountProfile with _$SnAccountProfile { sealed class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({ const factory SnAccountProfile({
@@ -38,7 +65,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
@Default('') String location, @Default('') String location,
@Default('') String timeZone, @Default('') String timeZone,
DateTime? birthday, DateTime? birthday,
@Default({}) Map<String, String> links, @ProfileLinkConverter() @Default([]) List<ProfileLink> links,
DateTime? lastSeenAt, DateTime? lastSeenAt,
SnAccountBadge? activeBadge, SnAccountBadge? activeBadge,
required int experience, required int experience,
@@ -148,3 +175,36 @@ sealed class SnVerificationMark with _$SnVerificationMark {
factory SnVerificationMark.fromJson(Map<String, dynamic> json) => factory SnVerificationMark.fromJson(Map<String, dynamic> json) =>
_$SnVerificationMarkFromJson(json); _$SnVerificationMarkFromJson(json);
} }
@freezed
sealed class SnAuthDevice with _$SnAuthDevice {
const factory SnAuthDevice({
required String id,
required String deviceId,
required String deviceName,
required String? deviceLabel,
required String accountId,
required int platform,
@Default(false) bool isCurrent,
}) = _SnAuthDevice;
factory SnAuthDevice.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceFromJson(json);
}
@freezed
sealed class SnAuthDeviceWithChallenge with _$SnAuthDeviceWithChallenge {
const factory SnAuthDeviceWithChallenge({
required String id,
required String deviceId,
required String deviceName,
required String? deviceLabel,
required String accountId,
required int platform,
required List<SnAuthChallenge> challenges,
@Default(false) bool isCurrent,
}) = _SnAuthDeviceWithChallengee;
factory SnAuthDeviceWithChallenge.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceWithChallengeFromJson(json);
}

View File

@@ -3,7 +3,7 @@
// ignore_for_file: type=lint // 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 // 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 'user.dart'; part of 'account.dart';
// ************************************************************************** // **************************************************************************
// FreezedGenerator // FreezedGenerator
@@ -347,10 +347,270 @@ $SnWalletSubscriptionRefCopyWith<$Res>? get perkSubscription {
} }
/// @nodoc
mixin _$ProfileLink {
String get name; String get url;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ProfileLinkCopyWith<ProfileLink> get copyWith => _$ProfileLinkCopyWithImpl<ProfileLink>(this as ProfileLink, _$identity);
/// Serializes this ProfileLink to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,url);
@override
String toString() {
return 'ProfileLink(name: $name, url: $url)';
}
}
/// @nodoc
abstract mixin class $ProfileLinkCopyWith<$Res> {
factory $ProfileLinkCopyWith(ProfileLink value, $Res Function(ProfileLink) _then) = _$ProfileLinkCopyWithImpl;
@useResult
$Res call({
String name, String url
});
}
/// @nodoc
class _$ProfileLinkCopyWithImpl<$Res>
implements $ProfileLinkCopyWith<$Res> {
_$ProfileLinkCopyWithImpl(this._self, this._then);
final ProfileLink _self;
final $Res Function(ProfileLink) _then;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? url = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [ProfileLink].
extension ProfileLinkPatterns on ProfileLink {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ProfileLink value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ProfileLink value) $default,){
final _that = this;
switch (_that) {
case _ProfileLink():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ProfileLink value)? $default,){
final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String url)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that.name,_that.url);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String url) $default,) {final _that = this;
switch (_that) {
case _ProfileLink():
return $default(_that.name,_that.url);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String url)? $default,) {final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that.name,_that.url);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ProfileLink implements ProfileLink {
const _ProfileLink({required this.name, required this.url});
factory _ProfileLink.fromJson(Map<String, dynamic> json) => _$ProfileLinkFromJson(json);
@override final String name;
@override final String url;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ProfileLinkCopyWith<_ProfileLink> get copyWith => __$ProfileLinkCopyWithImpl<_ProfileLink>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ProfileLinkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,url);
@override
String toString() {
return 'ProfileLink(name: $name, url: $url)';
}
}
/// @nodoc
abstract mixin class _$ProfileLinkCopyWith<$Res> implements $ProfileLinkCopyWith<$Res> {
factory _$ProfileLinkCopyWith(_ProfileLink value, $Res Function(_ProfileLink) _then) = __$ProfileLinkCopyWithImpl;
@override @useResult
$Res call({
String name, String url
});
}
/// @nodoc
class __$ProfileLinkCopyWithImpl<$Res>
implements _$ProfileLinkCopyWith<$Res> {
__$ProfileLinkCopyWithImpl(this._self, this._then);
final _ProfileLink _self;
final $Res Function(_ProfileLink) _then;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? url = null,}) {
return _then(_ProfileLink(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc /// @nodoc
mixin _$SnAccountProfile { mixin _$SnAccountProfile {
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; Map<String, String> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile /// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -383,7 +643,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl; factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -413,7 +673,7 @@ as String,location: null == location ? _self.location : location // ignore: cast
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
@@ -554,7 +814,7 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _SnAccountProfile() when $default != null: case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -575,7 +835,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAccountProfile(): case _SnAccountProfile():
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);} return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
@@ -592,7 +852,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAccountProfile() when $default != null: case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -607,7 +867,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
@JsonSerializable() @JsonSerializable()
class _SnAccountProfile implements SnAccountProfile { class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, final Map<String, String> links = const {}, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json); factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id; @override final String id;
@@ -620,11 +880,11 @@ class _SnAccountProfile implements SnAccountProfile {
@override@JsonKey() final String location; @override@JsonKey() final String location;
@override@JsonKey() final String timeZone; @override@JsonKey() final String timeZone;
@override final DateTime? birthday; @override final DateTime? birthday;
final Map<String, String> _links; final List<ProfileLink> _links;
@override@JsonKey() Map<String, String> get links { @override@JsonKey()@ProfileLinkConverter() List<ProfileLink> get links {
if (_links is EqualUnmodifiableMapView) return _links; if (_links is EqualUnmodifiableListView) return _links;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_links); return EqualUnmodifiableListView(_links);
} }
@override final DateTime? lastSeenAt; @override final DateTime? lastSeenAt;
@@ -672,7 +932,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl; factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -702,7 +962,7 @@ as String,location: null == location ? _self.location : location // ignore: cast
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
@@ -2192,6 +2452,572 @@ as String?,
} }
}
/// @nodoc
mixin _$SnAuthDevice {
String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; bool get isCurrent;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAuthDeviceCopyWith<SnAuthDevice> get copyWith => _$SnAuthDeviceCopyWithImpl<SnAuthDevice>(this as SnAuthDevice, _$identity);
/// Serializes this SnAuthDevice to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,isCurrent);
@override
String toString() {
return 'SnAuthDevice(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class $SnAuthDeviceCopyWith<$Res> {
factory $SnAuthDeviceCopyWith(SnAuthDevice value, $Res Function(SnAuthDevice) _then) = _$SnAuthDeviceCopyWithImpl;
@useResult
$Res call({
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent
});
}
/// @nodoc
class _$SnAuthDeviceCopyWithImpl<$Res>
implements $SnAuthDeviceCopyWith<$Res> {
_$SnAuthDeviceCopyWithImpl(this._self, this._then);
final SnAuthDevice _self;
final $Res Function(SnAuthDevice) _then;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? isCurrent = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [SnAuthDevice].
extension SnAuthDevicePatterns on SnAuthDevice {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDevice value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDevice value) $default,){
final _that = this;
switch (_that) {
case _SnAuthDevice():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDevice value)? $default,){
final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent) $default,) {final _that = this;
switch (_that) {
case _SnAuthDevice():
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent)? $default,) {final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnAuthDevice implements SnAuthDevice {
const _SnAuthDevice({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, this.isCurrent = false});
factory _SnAuthDevice.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceFromJson(json);
@override final String id;
@override final String deviceId;
@override final String deviceName;
@override final String? deviceLabel;
@override final String accountId;
@override final int platform;
@override@JsonKey() final bool isCurrent;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAuthDeviceCopyWith<_SnAuthDevice> get copyWith => __$SnAuthDeviceCopyWithImpl<_SnAuthDevice>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAuthDeviceToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,isCurrent);
@override
String toString() {
return 'SnAuthDevice(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class _$SnAuthDeviceCopyWith<$Res> implements $SnAuthDeviceCopyWith<$Res> {
factory _$SnAuthDeviceCopyWith(_SnAuthDevice value, $Res Function(_SnAuthDevice) _then) = __$SnAuthDeviceCopyWithImpl;
@override @useResult
$Res call({
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent
});
}
/// @nodoc
class __$SnAuthDeviceCopyWithImpl<$Res>
implements _$SnAuthDeviceCopyWith<$Res> {
__$SnAuthDeviceCopyWithImpl(this._self, this._then);
final _SnAuthDevice _self;
final $Res Function(_SnAuthDevice) _then;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? isCurrent = null,}) {
return _then(_SnAuthDevice(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
SnAuthDeviceWithChallenge _$SnAuthDeviceWithChallengeFromJson(
Map<String, dynamic> json
) {
return _SnAuthDeviceWithChallengee.fromJson(
json
);
}
/// @nodoc
mixin _$SnAuthDeviceWithChallenge {
String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; List<SnAuthChallenge> get challenges; bool get isCurrent;
/// Create a copy of SnAuthDeviceWithChallenge
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAuthDeviceWithChallengeCopyWith<SnAuthDeviceWithChallenge> get copyWith => _$SnAuthDeviceWithChallengeCopyWithImpl<SnAuthDeviceWithChallenge>(this as SnAuthDeviceWithChallenge, _$identity);
/// Serializes this SnAuthDeviceWithChallenge to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDeviceWithChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.challenges, challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(challenges),isCurrent);
@override
String toString() {
return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class $SnAuthDeviceWithChallengeCopyWith<$Res> {
factory $SnAuthDeviceWithChallengeCopyWith(SnAuthDeviceWithChallenge value, $Res Function(SnAuthDeviceWithChallenge) _then) = _$SnAuthDeviceWithChallengeCopyWithImpl;
@useResult
$Res call({
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent
});
}
/// @nodoc
class _$SnAuthDeviceWithChallengeCopyWithImpl<$Res>
implements $SnAuthDeviceWithChallengeCopyWith<$Res> {
_$SnAuthDeviceWithChallengeCopyWithImpl(this._self, this._then);
final SnAuthDeviceWithChallenge _self;
final $Res Function(SnAuthDeviceWithChallenge) _then;
/// Create a copy of SnAuthDeviceWithChallenge
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,challenges: null == challenges ? _self.challenges : challenges // ignore: cast_nullable_to_non_nullable
as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [SnAuthDeviceWithChallenge].
extension SnAuthDeviceWithChallengePatterns on SnAuthDeviceWithChallenge {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDeviceWithChallengee value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDeviceWithChallengee value) $default,){
final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDeviceWithChallengee value)? $default,){
final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent) $default,) {final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee():
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent)? $default,) {final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnAuthDeviceWithChallengee implements SnAuthDeviceWithChallenge {
const _SnAuthDeviceWithChallengee({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, required final List<SnAuthChallenge> challenges, this.isCurrent = false}): _challenges = challenges;
factory _SnAuthDeviceWithChallengee.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceWithChallengeeFromJson(json);
@override final String id;
@override final String deviceId;
@override final String deviceName;
@override final String? deviceLabel;
@override final String accountId;
@override final int platform;
final List<SnAuthChallenge> _challenges;
@override List<SnAuthChallenge> get challenges {
if (_challenges is EqualUnmodifiableListView) return _challenges;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_challenges);
}
@override@JsonKey() final bool isCurrent;
/// Create a copy of SnAuthDeviceWithChallenge
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAuthDeviceWithChallengeeCopyWith<_SnAuthDeviceWithChallengee> get copyWith => __$SnAuthDeviceWithChallengeeCopyWithImpl<_SnAuthDeviceWithChallengee>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAuthDeviceWithChallengeeToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDeviceWithChallengee&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._challenges, _challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(_challenges),isCurrent);
@override
String toString() {
return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class _$SnAuthDeviceWithChallengeeCopyWith<$Res> implements $SnAuthDeviceWithChallengeCopyWith<$Res> {
factory _$SnAuthDeviceWithChallengeeCopyWith(_SnAuthDeviceWithChallengee value, $Res Function(_SnAuthDeviceWithChallengee) _then) = __$SnAuthDeviceWithChallengeeCopyWithImpl;
@override @useResult
$Res call({
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent
});
}
/// @nodoc
class __$SnAuthDeviceWithChallengeeCopyWithImpl<$Res>
implements _$SnAuthDeviceWithChallengeeCopyWith<$Res> {
__$SnAuthDeviceWithChallengeeCopyWithImpl(this._self, this._then);
final _SnAuthDeviceWithChallengee _self;
final $Res Function(_SnAuthDeviceWithChallengee) _then;
/// Create a copy of SnAuthDeviceWithChallenge
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) {
return _then(_SnAuthDeviceWithChallengee(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,challenges: null == challenges ? _self._challenges : challenges // ignore: cast_nullable_to_non_nullable
as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
} }
// dart format on // dart format on

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart'; part of 'account.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator
@@ -47,6 +47,12 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) =>
_ProfileLink(name: json['name'] as String, url: json['url'] as String);
Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
<String, dynamic>{'name': instance.name, 'url': instance.url};
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) => _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile( _SnAccountProfile(
id: json['id'] as String, id: json['id'] as String,
@@ -63,10 +69,9 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
? null ? null
: DateTime.parse(json['birthday'] as String), : DateTime.parse(json['birthday'] as String),
links: links:
(json['links'] as Map<String, dynamic>?)?.map( json['links'] == null
(k, e) => MapEntry(k, e as String), ? const []
) ?? : const ProfileLinkConverter().fromJson(json['links']),
const {},
lastSeenAt: lastSeenAt:
json['last_seen_at'] == null json['last_seen_at'] == null
? null ? null
@@ -116,7 +121,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'location': instance.location, 'location': instance.location,
'time_zone': instance.timeZone, 'time_zone': instance.timeZone,
'birthday': instance.birthday?.toIso8601String(), 'birthday': instance.birthday?.toIso8601String(),
'links': instance.links, 'links': const ProfileLinkConverter().toJson(instance.links),
'last_seen_at': instance.lastSeenAt?.toIso8601String(), 'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'active_badge': instance.activeBadge?.toJson(), 'active_badge': instance.activeBadge?.toJson(),
'experience': instance.experience, 'experience': instance.experience,
@@ -292,3 +297,54 @@ Map<String, dynamic> _$SnVerificationMarkToJson(_SnVerificationMark instance) =>
'description': instance.description, 'description': instance.description,
'verified_by': instance.verifiedBy, 'verified_by': instance.verifiedBy,
}; };
_SnAuthDevice _$SnAuthDeviceFromJson(Map<String, dynamic> json) =>
_SnAuthDevice(
id: json['id'] as String,
deviceId: json['device_id'] as String,
deviceName: json['device_name'] as String,
deviceLabel: json['device_label'] as String?,
accountId: json['account_id'] as String,
platform: (json['platform'] as num).toInt(),
isCurrent: json['is_current'] as bool? ?? false,
);
Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) =>
<String, dynamic>{
'id': instance.id,
'device_id': instance.deviceId,
'device_name': instance.deviceName,
'device_label': instance.deviceLabel,
'account_id': instance.accountId,
'platform': instance.platform,
'is_current': instance.isCurrent,
};
_SnAuthDeviceWithChallengee _$SnAuthDeviceWithChallengeeFromJson(
Map<String, dynamic> json,
) => _SnAuthDeviceWithChallengee(
id: json['id'] as String,
deviceId: json['device_id'] as String,
deviceName: json['device_name'] as String,
deviceLabel: json['device_label'] as String?,
accountId: json['account_id'] as String,
platform: (json['platform'] as num).toInt(),
challenges:
(json['challenges'] as List<dynamic>)
.map((e) => SnAuthChallenge.fromJson(e as Map<String, dynamic>))
.toList(),
isCurrent: json['is_current'] as bool? ?? false,
);
Map<String, dynamic> _$SnAuthDeviceWithChallengeeToJson(
_SnAuthDeviceWithChallengee instance,
) => <String, dynamic>{
'id': instance.id,
'device_id': instance.deviceId,
'device_name': instance.deviceName,
'device_label': instance.deviceLabel,
'account_id': instance.accountId,
'platform': instance.platform,
'challenges': instance.challenges.map((e) => e.toJson()).toList(),
'is_current': instance.isCurrent,
};

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
part 'activity.freezed.dart'; part 'activity.freezed.dart';
part 'activity.g.dart'; part 'activity.g.dart';

View File

@@ -19,14 +19,12 @@ sealed class SnAuthChallenge with _$SnAuthChallenge {
required int stepRemain, required int stepRemain,
required int stepTotal, required int stepTotal,
required int failedAttempts, required int failedAttempts,
required int platform,
required int type, required int type,
required List<String> blacklistFactors, required List<String> blacklistFactors,
required List<dynamic> audiences, required List<dynamic> audiences,
required List<dynamic> scopes, required List<dynamic> scopes,
required String ipAddress, required String ipAddress,
required String userAgent, required String userAgent,
required String deviceId,
required String? nonce, required String? nonce,
required String? location, required String? location,
required String accountId, required String accountId,
@@ -76,22 +74,6 @@ sealed class SnAuthFactor with _$SnAuthFactor {
_$SnAuthFactorFromJson(json); _$SnAuthFactorFromJson(json);
} }
@freezed
sealed class SnAuthDevice with _$SnAuthDevice {
const factory SnAuthDevice({
required dynamic label,
required String userAgent,
required String deviceId,
required int platform,
required List<SnAuthSession> sessions,
// Not from backend, used for UI
@Default(false) bool isCurrent,
}) = _SnAuthDevice;
factory SnAuthDevice.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceFromJson(json);
}
@freezed @freezed
sealed class SnAccountConnection with _$SnAccountConnection { sealed class SnAccountConnection with _$SnAccountConnection {
const factory SnAccountConnection({ const factory SnAccountConnection({

View File

@@ -272,7 +272,7 @@ as String,
/// @nodoc /// @nodoc
mixin _$SnAuthChallenge { mixin _$SnAuthChallenge {
String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get platform; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String get deviceId; String? get nonce; String? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; String? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -285,16 +285,16 @@ $SnAuthChallengeCopyWith<SnAuthChallenge> get copyWith => _$SnAuthChallengeCopyW
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]); int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,type,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,nonce,location,accountId,createdAt,updatedAt,deletedAt);
@override @override
String toString() { String toString() {
return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@@ -305,7 +305,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res> {
factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -322,21 +322,19 @@ class _$SnAuthChallengeCopyWithImpl<$Res>
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,blacklistFactors: null == blacklistFactors ? _self.blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable as int,blacklistFactors: null == blacklistFactors ? _self.blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable
as List<String>,audiences: null == audiences ? _self.audiences : audiences // ignore: cast_nullable_to_non_nullable as List<String>,audiences: null == audiences ? _self.audiences : audiences // ignore: cast_nullable_to_non_nullable
as List<dynamic>,scopes: null == scopes ? _self.scopes : scopes // ignore: cast_nullable_to_non_nullable as List<dynamic>,scopes: null == scopes ? _self.scopes : scopes // ignore: cast_nullable_to_non_nullable
as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable
as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
@@ -425,10 +423,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthChallenge() when $default != null: case _SnAuthChallenge() when $default != null:
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse(); return orElse();
} }
@@ -446,10 +444,10 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthChallenge(): case _SnAuthChallenge():
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@@ -463,10 +461,10 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthChallenge() when $default != null: case _SnAuthChallenge() when $default != null:
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null; return null;
} }
@@ -478,7 +476,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
@JsonSerializable() @JsonSerializable()
class _SnAuthChallenge implements SnAuthChallenge { class _SnAuthChallenge implements SnAuthChallenge {
const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required this.failedAttempts, required this.platform, required this.type, required final List<String> blacklistFactors, required final List<dynamic> audiences, required final List<dynamic> scopes, required this.ipAddress, required this.userAgent, required this.deviceId, required this.nonce, required this.location, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes; const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required this.failedAttempts, required this.type, required final List<String> blacklistFactors, required final List<dynamic> audiences, required final List<dynamic> scopes, required this.ipAddress, required this.userAgent, required this.nonce, required this.location, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes;
factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json); factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
@override final String id; @override final String id;
@@ -486,7 +484,6 @@ class _SnAuthChallenge implements SnAuthChallenge {
@override final int stepRemain; @override final int stepRemain;
@override final int stepTotal; @override final int stepTotal;
@override final int failedAttempts; @override final int failedAttempts;
@override final int platform;
@override final int type; @override final int type;
final List<String> _blacklistFactors; final List<String> _blacklistFactors;
@override List<String> get blacklistFactors { @override List<String> get blacklistFactors {
@@ -511,7 +508,6 @@ class _SnAuthChallenge implements SnAuthChallenge {
@override final String ipAddress; @override final String ipAddress;
@override final String userAgent; @override final String userAgent;
@override final String deviceId;
@override final String? nonce; @override final String? nonce;
@override final String? location; @override final String? location;
@override final String accountId; @override final String accountId;
@@ -532,16 +528,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]); int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,type,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,nonce,location,accountId,createdAt,updatedAt,deletedAt);
@override @override
String toString() { String toString() {
return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@@ -552,7 +548,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -569,21 +565,19 @@ class __$SnAuthChallengeCopyWithImpl<$Res>
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAuthChallenge( return _then(_SnAuthChallenge(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,blacklistFactors: null == blacklistFactors ? _self._blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable as int,blacklistFactors: null == blacklistFactors ? _self._blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable
as List<String>,audiences: null == audiences ? _self._audiences : audiences // ignore: cast_nullable_to_non_nullable as List<String>,audiences: null == audiences ? _self._audiences : audiences // ignore: cast_nullable_to_non_nullable
as List<dynamic>,scopes: null == scopes ? _self._scopes : scopes // ignore: cast_nullable_to_non_nullable as List<dynamic>,scopes: null == scopes ? _self._scopes : scopes // ignore: cast_nullable_to_non_nullable
as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable
as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
@@ -1189,286 +1183,6 @@ as Map<String, dynamic>?,
} }
/// @nodoc
mixin _$SnAuthDevice {
dynamic get label; String get userAgent; String get deviceId; int get platform; List<SnAuthSession> get sessions;// Not from backend, used for UI
bool get isCurrent;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAuthDeviceCopyWith<SnAuthDevice> get copyWith => _$SnAuthDeviceCopyWithImpl<SnAuthDevice>(this as SnAuthDevice, _$identity);
/// Serializes this SnAuthDevice to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.sessions, sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(sessions),isCurrent);
@override
String toString() {
return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class $SnAuthDeviceCopyWith<$Res> {
factory $SnAuthDeviceCopyWith(SnAuthDevice value, $Res Function(SnAuthDevice) _then) = _$SnAuthDeviceCopyWithImpl;
@useResult
$Res call({
dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent
});
}
/// @nodoc
class _$SnAuthDeviceCopyWithImpl<$Res>
implements $SnAuthDeviceCopyWith<$Res> {
_$SnAuthDeviceCopyWithImpl(this._self, this._then);
final SnAuthDevice _self;
final $Res Function(SnAuthDevice) _then;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
return _then(_self.copyWith(
label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,sessions: null == sessions ? _self.sessions : sessions // ignore: cast_nullable_to_non_nullable
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [SnAuthDevice].
extension SnAuthDevicePatterns on SnAuthDevice {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDevice value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDevice value) $default,){
final _that = this;
switch (_that) {
case _SnAuthDevice():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDevice value)? $default,){
final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent) $default,) {final _that = this;
switch (_that) {
case _SnAuthDevice():
return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent)? $default,) {final _that = this;
switch (_that) {
case _SnAuthDevice() when $default != null:
return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnAuthDevice implements SnAuthDevice {
const _SnAuthDevice({required this.label, required this.userAgent, required this.deviceId, required this.platform, required final List<SnAuthSession> sessions, this.isCurrent = false}): _sessions = sessions;
factory _SnAuthDevice.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceFromJson(json);
@override final dynamic label;
@override final String userAgent;
@override final String deviceId;
@override final int platform;
final List<SnAuthSession> _sessions;
@override List<SnAuthSession> get sessions {
if (_sessions is EqualUnmodifiableListView) return _sessions;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_sessions);
}
// Not from backend, used for UI
@override@JsonKey() final bool isCurrent;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAuthDeviceCopyWith<_SnAuthDevice> get copyWith => __$SnAuthDeviceCopyWithImpl<_SnAuthDevice>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAuthDeviceToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._sessions, _sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(_sessions),isCurrent);
@override
String toString() {
return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class _$SnAuthDeviceCopyWith<$Res> implements $SnAuthDeviceCopyWith<$Res> {
factory _$SnAuthDeviceCopyWith(_SnAuthDevice value, $Res Function(_SnAuthDevice) _then) = __$SnAuthDeviceCopyWithImpl;
@override @useResult
$Res call({
dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent
});
}
/// @nodoc
class __$SnAuthDeviceCopyWithImpl<$Res>
implements _$SnAuthDeviceCopyWith<$Res> {
__$SnAuthDeviceCopyWithImpl(this._self, this._then);
final _SnAuthDevice _self;
final $Res Function(_SnAuthDevice) _then;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
return _then(_SnAuthDevice(
label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,sessions: null == sessions ? _self._sessions : sessions // ignore: cast_nullable_to_non_nullable
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc /// @nodoc
mixin _$SnAccountConnection { mixin _$SnAccountConnection {

View File

@@ -20,7 +20,6 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
stepRemain: (json['step_remain'] as num).toInt(), stepRemain: (json['step_remain'] as num).toInt(),
stepTotal: (json['step_total'] as num).toInt(), stepTotal: (json['step_total'] as num).toInt(),
failedAttempts: (json['failed_attempts'] as num).toInt(), failedAttempts: (json['failed_attempts'] as num).toInt(),
platform: (json['platform'] as num).toInt(),
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
blacklistFactors: blacklistFactors:
(json['blacklist_factors'] as List<dynamic>) (json['blacklist_factors'] as List<dynamic>)
@@ -30,7 +29,6 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
scopes: json['scopes'] as List<dynamic>, scopes: json['scopes'] as List<dynamic>,
ipAddress: json['ip_address'] as String, ipAddress: json['ip_address'] as String,
userAgent: json['user_agent'] as String, userAgent: json['user_agent'] as String,
deviceId: json['device_id'] as String,
nonce: json['nonce'] as String?, nonce: json['nonce'] as String?,
location: json['location'] as String?, location: json['location'] as String?,
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
@@ -49,14 +47,12 @@ Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
'step_remain': instance.stepRemain, 'step_remain': instance.stepRemain,
'step_total': instance.stepTotal, 'step_total': instance.stepTotal,
'failed_attempts': instance.failedAttempts, 'failed_attempts': instance.failedAttempts,
'platform': instance.platform,
'type': instance.type, 'type': instance.type,
'blacklist_factors': instance.blacklistFactors, 'blacklist_factors': instance.blacklistFactors,
'audiences': instance.audiences, 'audiences': instance.audiences,
'scopes': instance.scopes, 'scopes': instance.scopes,
'ip_address': instance.ipAddress, 'ip_address': instance.ipAddress,
'user_agent': instance.userAgent, 'user_agent': instance.userAgent,
'device_id': instance.deviceId,
'nonce': instance.nonce, 'nonce': instance.nonce,
'location': instance.location, 'location': instance.location,
'account_id': instance.accountId, 'account_id': instance.accountId,
@@ -133,29 +129,6 @@ Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) =>
'created_response': instance.createdResponse, 'created_response': instance.createdResponse,
}; };
_SnAuthDevice _$SnAuthDeviceFromJson(Map<String, dynamic> json) =>
_SnAuthDevice(
label: json['label'],
userAgent: json['user_agent'] as String,
deviceId: json['device_id'] as String,
platform: (json['platform'] as num).toInt(),
sessions:
(json['sessions'] as List<dynamic>)
.map((e) => SnAuthSession.fromJson(e as Map<String, dynamic>))
.toList(),
isCurrent: json['is_current'] as bool? ?? false,
);
Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) =>
<String, dynamic>{
'label': instance.label,
'user_agent': instance.userAgent,
'device_id': instance.deviceId,
'platform': instance.platform,
'sessions': instance.sessions.map((e) => e.toJson()).toList(),
'is_current': instance.isCurrent,
};
_SnAccountConnection _$SnAccountConnectionFromJson(Map<String, dynamic> json) => _SnAccountConnection _$SnAccountConnectionFromJson(Map<String, dynamic> json) =>
_SnAccountConnection( _SnAccountConnection(
id: json['id'] as String, id: json['id'] as String,

View File

@@ -1,7 +1,7 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
part 'chat.freezed.dart'; part 'chat.freezed.dart';
part 'chat.g.dart'; part 'chat.g.dart';

View File

@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
part 'custom_app.freezed.dart'; part 'custom_app.freezed.dart';
part 'custom_app.g.dart'; part 'custom_app.g.dart';

View File

@@ -1,14 +1,26 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/publisher.dart';
part 'developer.freezed.dart'; part 'developer.freezed.dart';
part 'developer.g.dart'; part 'developer.g.dart';
@freezed
sealed class SnDeveloper with _$SnDeveloper {
const factory SnDeveloper({
required String id,
required String publisherId,
SnPublisher? publisher,
}) = _SnDeveloper;
factory SnDeveloper.fromJson(Map<String, dynamic> json) =>
_$SnDeveloperFromJson(json);
}
@freezed @freezed
sealed class DeveloperStats with _$DeveloperStats { sealed class DeveloperStats with _$DeveloperStats {
const factory DeveloperStats({ const factory DeveloperStats({@Default(0) int totalCustomApps}) =
@Default(0) int totalCustomApps, _DeveloperStats;
}) = _DeveloperStats;
factory DeveloperStats.fromJson(Map<String, dynamic> json) => factory DeveloperStats.fromJson(Map<String, dynamic> json) =>
_$DeveloperStatsFromJson(json); _$DeveloperStatsFromJson(json);
} }

View File

@@ -12,6 +12,293 @@ part of 'developer.dart';
// dart format off // dart format off
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnDeveloper {
String get id; String get publisherId; SnPublisher? get publisher;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnDeveloperCopyWith<SnDeveloper> get copyWith => _$SnDeveloperCopyWithImpl<SnDeveloper>(this as SnDeveloper, _$identity);
/// Serializes this SnDeveloper to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
@override
String toString() {
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class $SnDeveloperCopyWith<$Res> {
factory $SnDeveloperCopyWith(SnDeveloper value, $Res Function(SnDeveloper) _then) = _$SnDeveloperCopyWithImpl;
@useResult
$Res call({
String id, String publisherId, SnPublisher? publisher
});
$SnPublisherCopyWith<$Res>? get publisher;
}
/// @nodoc
class _$SnDeveloperCopyWithImpl<$Res>
implements $SnDeveloperCopyWith<$Res> {
_$SnDeveloperCopyWithImpl(this._self, this._then);
final SnDeveloper _self;
final $Res Function(SnDeveloper) _then;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,
));
}
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}
}
/// Adds pattern-matching-related methods to [SnDeveloper].
extension SnDeveloperPatterns on SnDeveloper {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnDeveloper value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnDeveloper value) $default,){
final _that = this;
switch (_that) {
case _SnDeveloper():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnDeveloper value)? $default,){
final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String publisherId, SnPublisher? publisher)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that.id,_that.publisherId,_that.publisher);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String publisherId, SnPublisher? publisher) $default,) {final _that = this;
switch (_that) {
case _SnDeveloper():
return $default(_that.id,_that.publisherId,_that.publisher);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String publisherId, SnPublisher? publisher)? $default,) {final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that.id,_that.publisherId,_that.publisher);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnDeveloper implements SnDeveloper {
const _SnDeveloper({required this.id, required this.publisherId, this.publisher});
factory _SnDeveloper.fromJson(Map<String, dynamic> json) => _$SnDeveloperFromJson(json);
@override final String id;
@override final String publisherId;
@override final SnPublisher? publisher;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnDeveloperCopyWith<_SnDeveloper> get copyWith => __$SnDeveloperCopyWithImpl<_SnDeveloper>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnDeveloperToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
@override
String toString() {
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class _$SnDeveloperCopyWith<$Res> implements $SnDeveloperCopyWith<$Res> {
factory _$SnDeveloperCopyWith(_SnDeveloper value, $Res Function(_SnDeveloper) _then) = __$SnDeveloperCopyWithImpl;
@override @useResult
$Res call({
String id, String publisherId, SnPublisher? publisher
});
@override $SnPublisherCopyWith<$Res>? get publisher;
}
/// @nodoc
class __$SnDeveloperCopyWithImpl<$Res>
implements _$SnDeveloperCopyWith<$Res> {
__$SnDeveloperCopyWithImpl(this._self, this._then);
final _SnDeveloper _self;
final $Res Function(_SnDeveloper) _then;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
return _then(_SnDeveloper(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,
));
}
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}
}
/// @nodoc /// @nodoc
mixin _$DeveloperStats { mixin _$DeveloperStats {

View File

@@ -6,6 +6,22 @@ part of 'developer.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_SnDeveloper _$SnDeveloperFromJson(Map<String, dynamic> json) => _SnDeveloper(
id: json['id'] as String,
publisherId: json['publisher_id'] as String,
publisher:
json['publisher'] == null
? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
);
Map<String, dynamic> _$SnDeveloperToJson(_SnDeveloper instance) =>
<String, dynamic>{
'id': instance.id,
'publisher_id': instance.publisherId,
'publisher': instance.publisher?.toJson(),
};
_DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) => _DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) =>
_DeveloperStats( _DeveloperStats(
totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0, totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0,

View File

@@ -8,7 +8,7 @@ part 'poll.g.dart';
sealed class SnPollWithStats with _$SnPollWithStats { sealed class SnPollWithStats with _$SnPollWithStats {
const factory SnPollWithStats({ const factory SnPollWithStats({
required Map<String, dynamic>? userAnswer, required Map<String, dynamic>? userAnswer,
required Map<String, dynamic> stats, @Default({}) Map<String, dynamic> stats,
required String id, required String id,
required List<SnPollQuestion> questions, required List<SnPollQuestion> questions,
String? title, String? title,

View File

@@ -213,7 +213,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
@JsonSerializable() @JsonSerializable()
class _SnPollWithStats implements SnPollWithStats { class _SnPollWithStats implements SnPollWithStats {
const _SnPollWithStats({required final Map<String, dynamic>? userAnswer, required final Map<String, dynamic> stats, required this.id, required final List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions; const _SnPollWithStats({required final Map<String, dynamic>? userAnswer, final Map<String, dynamic> stats = const {}, required this.id, required final List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions;
factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json); factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
final Map<String, dynamic>? _userAnswer; final Map<String, dynamic>? _userAnswer;
@@ -226,7 +226,7 @@ class _SnPollWithStats implements SnPollWithStats {
} }
final Map<String, dynamic> _stats; final Map<String, dynamic> _stats;
@override Map<String, dynamic> get stats { @override@JsonKey() Map<String, dynamic> get stats {
if (_stats is EqualUnmodifiableMapView) return _stats; if (_stats is EqualUnmodifiableMapView) return _stats;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_stats); return EqualUnmodifiableMapView(_stats);

View File

@@ -9,7 +9,7 @@ part of 'poll.dart';
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) => _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
_SnPollWithStats( _SnPollWithStats(
userAnswer: json['user_answer'] as Map<String, dynamic>?, userAnswer: json['user_answer'] as Map<String, dynamic>?,
stats: json['stats'] as Map<String, dynamic>, stats: json['stats'] as Map<String, dynamic>? ?? const {},
id: json['id'] as String, id: json['id'] as String,
questions: questions:
(json['questions'] as List<dynamic>) (json['questions'] as List<dynamic>)

View File

@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
part 'publisher.freezed.dart'; part 'publisher.freezed.dart';
part 'publisher.g.dart'; part 'publisher.g.dart';

View File

@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
part 'realm.freezed.dart'; part 'realm.freezed.dart';
part 'realm.g.dart'; part 'realm.g.dart';

View File

@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
part 'relationship.freezed.dart'; part 'relationship.freezed.dart';
part 'relationship.g.dart'; part 'relationship.g.dart';

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
part 'wallet.freezed.dart'; part 'wallet.freezed.dart';
part 'wallet.g.dart'; part 'wallet.g.dart';

View File

@@ -1,8 +1,9 @@
import 'dart:developer'; import 'dart:developer';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
@@ -17,6 +18,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final response = await client.get('/id/accounts/me'); final response = await client.get('/id/accounts/me');
final user = SnAccount.fromJson(response.data); final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user); state = AsyncValue.data(user);
FirebaseAnalytics.instance.setUserId(id: user.id);
} catch (error, stackTrace) { } catch (error, stackTrace) {
log( log(
"[UserInfo] Failed to fetch user info...", "[UserInfo] Failed to fetch user info...",
@@ -33,6 +35,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final prefs = _ref.read(sharedPreferencesProvider); final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey); await prefs.remove(kTokenPairStoreKey);
_ref.invalidate(tokenProvider); _ref.invalidate(tokenProvider);
FirebaseAnalytics.instance.setUserId(id: null);
} }
} }

View File

@@ -1,3 +1,5 @@
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -59,6 +61,9 @@ final routerProvider = Provider<GoRouter>((ref) {
return GoRouter( return GoRouter(
navigatorKey: rootNavigatorKey, navigatorKey: rootNavigatorKey,
initialLocation: '/', initialLocation: '/',
observers: [
FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance),
],
routes: [ routes: [
ShellRoute( ShellRoute(
navigatorKey: _shellNavigatorKey, navigatorKey: _shellNavigatorKey,

View File

@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/udid.native.dart'; import 'package:island/services/udid.native.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/services/update_service.dart'; import 'package:island/services/update_service.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -205,33 +205,16 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
// Fetch latest release and show the unified sheet // Fetch latest release and show the unified sheet
final svc = UpdateService(); final svc = UpdateService();
// Reuse service fetch + compare to decide content // Reuse service fetch + compare to decide content
showLoadingModal(context);
final release = await svc.fetchLatestRelease(); final release = await svc.fetchLatestRelease();
if (!context.mounted) return;
hideLoadingModal(context);
if (release != null) { if (release != null) {
await svc.showUpdateSheet(context, release); await svc.showUpdateSheet(context, release);
} else { } else {
// Fallback: show a simple sheet indicating no info showInfoAlert(
// Use your SheetScaffold for consistent styling 'Currently cannot get update from the GitHub.',
// Show a minimal message 'Unable to check for updates',
// ignore: use_build_context_synchronously
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
showDragHandle: true,
backgroundColor:
Theme.of(context).colorScheme.surface,
builder:
(_) => const SheetScaffold(
titleText: 'Update',
child: Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Unable to fetch release info at this time.',
),
),
),
),
); );
} }
}, },

View File

@@ -3,12 +3,14 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/screens/notification.dart'; import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/account/leveling_progress.dart'; import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/debug_sheet.dart'; import 'package:island/widgets/debug_sheet.dart';
@@ -236,7 +238,7 @@ class AccountScreen extends HookConsumerWidget {
), ),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: Text('abuseReports').tr(), title: Text('abuseReport').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.gavel), leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
@@ -303,7 +305,12 @@ class AccountScreen extends HookConsumerWidget {
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('logout').tr(), title: Text('logout').tr(),
onTap: () { onTap: () async {
final apiClient = ref.watch(apiClientProvider);
showLoadingModal(context);
await apiClient.delete('/id/accounts/me/sessions/current');
if (!context.mounted) return;
hideLoadingModal(context);
final userNotifier = ref.read(userInfoProvider.notifier); final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.logOut(); userNotifier.logOut();
}, },

View File

@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart'; import 'package:island/models/auth.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/screens/account/me/settings_auth_factors.dart'; import 'package:island/screens/account/me/settings_auth_factors.dart';
@@ -15,7 +15,7 @@ import 'package:island/screens/account/me/settings_contacts.dart';
import 'package:island/screens/auth/captcha.dart'; import 'package:island/screens/auth/captcha.dart';
import 'package:island/screens/auth/login.dart'; import 'package:island/screens/auth/login.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_session_sheet.dart'; import 'package:island/widgets/account/account_devices.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';

View File

@@ -166,7 +166,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
webAuthenticationOptions: WebAuthenticationOptions( webAuthenticationOptions: WebAuthenticationOptions(
clientId: 'dev.solsynth.solarpass', clientId: 'dev.solsynth.solarpass',
redirectUri: Uri.parse( redirectUri: Uri.parse(
'https://nt.solian.app/auth/callback/apple', 'https://id.solian.app/auth/callback/apple',
), ),
), ),
); );

View File

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';

View File

@@ -7,6 +7,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
@@ -95,11 +96,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
final usernameController = useTextEditingController(text: user.value!.name); final usernameController = useTextEditingController(text: user.value!.name);
final nicknameController = useTextEditingController(text: user.value!.nick); final nicknameController = useTextEditingController(text: user.value!.nick);
final language = useState(user.value!.language); final language = useState(user.value!.language);
final links = useState<List<Map<String, String>>>( final links = useState<List<ProfileLink>>(user.value!.profile.links);
user.value!.profile.links.entries
.map((e) => {'key': e.key, 'value': e.value})
.toList(),
);
void updateBasicInfo() async { void updateBasicInfo() async {
if (!formKeyBasicInfo.currentState!.validate()) return; if (!formKeyBasicInfo.currentState!.validate()) return;
@@ -171,7 +168,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
'location': locationController.text, 'location': locationController.text,
'time_zone': timeZoneController.text, 'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(), 'birthday': birthday.value?.toUtc().toIso8601String(),
'links': {for (var e in links.value) e['key']!: e['value']!}, 'links': links.value,
}, },
); );
final userNotifier = ref.read(userInfoProvider.notifier); final userNotifier = ref.read(userInfoProvider.notifier);
@@ -575,13 +572,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: TextFormField( child: TextFormField(
initialValue: links.value[i]['key'], initialValue: links.value[i].name,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'linkKey'.tr(), labelText: 'linkKey'.tr(),
isDense: true, isDense: true,
), ),
onChanged: (value) { onChanged: (value) {
links.value[i]['key'] = value; links.value[i] = links.value[i].copyWith(
name: value,
);
}, },
onTapOutside: onTapOutside:
(_) => (_) =>
@@ -592,13 +591,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
Expanded( Expanded(
child: TextFormField( child: TextFormField(
initialValue: links.value[i]['value'], initialValue: links.value[i].url,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'linkValue'.tr(), labelText: 'linkValue'.tr(),
isDense: true, isDense: true,
), ),
onChanged: (value) { onChanged: (value) {
links.value[i]['value'] = value; links.value[i] = links.value[i].copyWith(
url: value,
);
}, },
onTapOutside: onTapOutside:
(_) => (_) =>
@@ -620,7 +621,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: FilledButton.icon( child: FilledButton.icon(
onPressed: () { onPressed: () {
links.value = List.from(links.value) links.value = List.from(links.value)
..add({'key': '', 'value': ''}); ..add(ProfileLink(name: '', url: ''));
}, },
label: Text('addLink').tr(), label: Text('addLink').tr(),
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),

View File

@@ -1,12 +1,14 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/models/relationship.dart'; import 'package:island/models/relationship.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/event_calendar.dart'; import 'package:island/pods/event_calendar.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
@@ -196,6 +198,15 @@ class AccountProfileScreen extends HookConsumerWidget {
List<Widget> buildSubcolumn(SnAccount data) { List<Widget> buildSubcolumn(SnAccount data) {
return [ return [
Row(
spacing: 6,
children: [
const Icon(Symbols.join, size: 17, fill: 1),
Text(
'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]),
),
],
),
if (data.profile.birthday != null) if (data.profile.birthday != null)
Row( Row(
spacing: 6, spacing: 6,
@@ -252,6 +263,10 @@ class AccountProfileScreen extends HookConsumerWidget {
} }
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
final isCurrentUser = useMemoized(
() => user.value?.id == account.value?.id,
[user, account],
);
Widget accountBasicInfo(SnAccount data) => Padding( Widget accountBasicInfo(SnAccount data) => Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
@@ -322,7 +337,7 @@ class AccountProfileScreen extends HookConsumerWidget {
spacing: 2, spacing: 2,
children: buildSubcolumn(data), children: buildSubcolumn(data),
), ),
if (data.profile.timeZone.isNotEmpty) if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -357,17 +372,21 @@ class AccountProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4), Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
for (final link in data.profile.links.entries) for (final link in data.profile.links)
ListTile( ListTile(
title: Text(link.key.capitalizeEachWord()), title: Text(link.name.capitalizeEachWord()),
subtitle: Text(link.value), subtitle: Text(link.url),
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
), ),
onTap: () { onTap: () {
launchUrlString(link.value); if (!link.url.startsWith('http') && !link.url.contains('://')) {
launchUrlString('https://${link.url}');
} else {
launchUrlString(link.url);
}
}, },
), ),
], ],
@@ -561,9 +580,10 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileBio(data).padding(top: 4), child: accountProfileBio(data).padding(top: 4),
), ),
SliverToBoxAdapter( if (data.profile.links.isNotEmpty)
child: accountProfileLinks(data), SliverToBoxAdapter(
), child: accountProfileLinks(data),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileDetail(data), child: accountProfileDetail(data),
), ),
@@ -574,7 +594,7 @@ class AccountProfileScreen extends HookConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverGap(24), SliverGap(24),
if (user.value != null) if (user.value != null && !isCurrentUser)
SliverToBoxAdapter(child: accountAction(data)), SliverToBoxAdapter(child: accountAction(data)),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Card( child: Card(
@@ -660,17 +680,18 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileBio(data).padding(horizontal: 4), child: accountProfileBio(data).padding(horizontal: 4),
), ),
SliverToBoxAdapter( if (data.profile.links.isNotEmpty)
child: accountProfileLinks( SliverToBoxAdapter(
data, child: accountProfileLinks(
).padding(horizontal: 4), data,
), ).padding(horizontal: 4),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileDetail( child: accountProfileDetail(
data, data,
).padding(horizontal: 4), ).padding(horizontal: 4),
), ),
if (user.value != null) if (user.value != null && !isCurrentUser)
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountAction(data).padding(horizontal: 4), child: accountAction(data).padding(horizontal: 4),
), ),

View File

@@ -216,6 +216,7 @@ class RelationshipScreen extends HookConsumerWidget {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true,
builder: (context) => AccountPickerSheet(), builder: (context) => AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@@ -42,6 +42,22 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), 4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
}; };
Future<String?> getDeviceName() async {
if (kIsWeb) return null;
String? name;
if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
name = deviceInfo.name;
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
name = deviceInfo.name;
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
name = deviceInfo.computerName;
}
return name;
}
class LoginScreen extends HookConsumerWidget { class LoginScreen extends HookConsumerWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@@ -198,28 +214,6 @@ class _LoginCheckScreen extends HookConsumerWidget {
wsNotifier.connect(); wsNotifier.connect();
if (context.mounted) Navigator.pop(context, true); if (context.mounted) Navigator.pop(context, true);
}); });
// Update the sessions' device name is available
if (!kIsWeb) {
String? name;
if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
name = deviceInfo.name;
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
name = deviceInfo.name;
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
name = deviceInfo.computerName;
}
if (name != null) {
final client = ref.watch(apiClientProvider);
await client.patch(
'/id/accounts/me/sessions/current/label',
data: jsonEncode(name),
);
}
}
} }
useEffect(() { useEffect(() {
@@ -578,6 +572,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
data: { data: {
'account': uname, 'account': uname,
'device_id': await getUdid(), 'device_id': await getUdid(),
'device_name': await getDeviceName(),
'platform': 'platform':
kIsWeb kIsWeb
? 1 ? 1
@@ -628,6 +623,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
'identity_token': credential.identityToken!, 'identity_token': credential.identityToken!,
'authorization_code': credential.authorizationCode, 'authorization_code': credential.authorizationCode,
'device_id': await getUdid(), 'device_id': await getUdid(),
'device_name': await getDeviceName(),
}, },
); );

View File

@@ -227,6 +227,7 @@ class ChatListScreen extends HookConsumerWidget {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@@ -339,7 +339,7 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
await apiClient.post( await apiClient.post(
'/chat/${chatRoom.value!.id}/members/me', '/sphere/chat/${chatRoom.value!.id}/members/me',
); );
ref.invalidate(chatroomIdentityProvider(id)); ref.invalidate(chatroomIdentityProvider(id));
} catch (err) { } catch (err) {
@@ -929,7 +929,7 @@ class ChatRoomScreen extends HookConsumerWidget {
if (attachment.isOnCloud) { if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
await client.delete( await client.delete(
'/files/${attachment.data.id}', '/drive/files/${attachment.data.id}',
); );
} }
final clone = List.of(attachments.value); final clone = List.of(attachments.value);

View File

@@ -589,6 +589,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;
@@ -727,7 +728,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
apiClientProvider, apiClientProvider,
); );
await apiClient.delete( await apiClient.delete(
'/chat/$roomId/members/${member.accountId}', '/sphere/chat/$roomId/members/${member.accountId}',
); );
// Refresh both providers // Refresh both providers
memberNotifier.reset(); memberNotifier.reset();

View File

@@ -382,7 +382,7 @@ class CreatorHubScreen extends HookConsumerWidget {
), ),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: const Text('Polls'), title: Text('polls').tr(),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.poll), leading: const Icon(Symbols.poll),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@@ -419,7 +419,7 @@ class CreatorHubScreen extends HookConsumerWidget {
), ),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: const Text('Web Feeds').tr(), title: const Text('webFeeds').tr(),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.rss_feed), leading: const Icon(Symbols.rss_feed),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@@ -659,7 +659,7 @@ class PublisherMemberNotifier extends StateNotifier<PublisherMemberState> {
try { try {
final response = await _apiClient.get( final response = await _apiClient.get(
'/publishers/$publisherUname/members', '/sphere/publishers/$publisherUname/members',
queryParameters: {'offset': offset, 'take': take}, queryParameters: {'offset': offset, 'take': take},
); );
@@ -708,6 +708,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
Future<void> invitePerson() async { Future<void> invitePerson() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
@@ -719,6 +720,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
'/publishers/$publisherUname/invites', '/publishers/$publisherUname/invites',
data: {'related_user_id': result.id, 'role': 0}, data: {'related_user_id': result.id, 'role': 0},
); );
// Refresh both providers
memberNotifier.reset();
await memberNotifier.loadMore();
ref.invalidate(memberListProvider); ref.invalidate(memberListProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@@ -822,6 +826,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
), ),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider); ref.invalidate(memberListProvider);
} }
}); });
@@ -843,6 +850,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
await apiClient.delete( await apiClient.delete(
'/publishers/$publisherUname/members/${member.accountId}', '/publishers/$publisherUname/members/${member.accountId}',
); );
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider); ref.invalidate(memberListProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/poll/poll_feedback.dart'; import 'package:island/widgets/poll/poll_feedback.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -13,17 +14,19 @@ part 'poll_list.g.dart';
@riverpod @riverpod
class PollListNotifier extends _$PollListNotifier class PollListNotifier extends _$PollListNotifier
with CursorPagingNotifierMixin<SnPoll> { with CursorPagingNotifierMixin<SnPollWithStats> {
static const int _pageSize = 20; static const int _pageSize = 20;
@override @override
Future<CursorPagingData<SnPoll>> build(String? pubName) { Future<CursorPagingData<SnPollWithStats>> build(String? pubName) {
// immediately load first page // immediately load first page
return fetch(cursor: null); return fetch(cursor: null);
} }
@override @override
Future<CursorPagingData<SnPoll>> fetch({required String? cursor}) async { Future<CursorPagingData<SnPollWithStats>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor); final offset = cursor == null ? 0 : int.parse(cursor);
@@ -41,7 +44,7 @@ class PollListNotifier extends _$PollListNotifier
); );
final total = int.parse(response.headers.value('X-Total') ?? '0'); final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data; final List<dynamic> data = response.data;
final items = data.map((json) => SnPoll.fromJson(json)).toList(); final items = data.map((json) => SnPollWithStats.fromJson(json)).toList();
final hasMore = offset + items.length < total; final hasMore = offset + items.length < total;
final nextCursor = hasMore ? (offset + items.length).toString() : null; final nextCursor = hasMore ? (offset + items.length).toString() : null;
@@ -54,6 +57,13 @@ class PollListNotifier extends _$PollListNotifier
} }
} }
@riverpod
Future<SnPollWithStats> pollWithStats(Ref ref, String id) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/polls/$id');
return SnPollWithStats.fromJson(resp.data);
}
class CreatorPollListScreen extends HookConsumerWidget { class CreatorPollListScreen extends HookConsumerWidget {
const CreatorPollListScreen({super.key, required this.pubName}); const CreatorPollListScreen({super.key, required this.pubName});
@@ -63,14 +73,14 @@ class CreatorPollListScreen extends HookConsumerWidget {
final result = await GoRouter.of( final result = await GoRouter.of(
context, context,
).pushNamed('creatorPollNew', pathParameters: {'name': pubName}); ).pushNamed('creatorPollNew', pathParameters: {'name': pubName});
if (result is SnPoll && context.mounted) { if (result is SnPollWithStats && context.mounted) {
Navigator.of(context).maybePop(result); Navigator.of(context).maybePop(result);
} }
} }
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Scaffold( return AppScaffold(
appBar: AppBar(title: const Text('Polls')), appBar: AppBar(title: const Text('Polls')),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _createPoll(context), onPressed: () => _createPoll(context),
@@ -91,8 +101,11 @@ class CreatorPollListScreen extends HookConsumerWidget {
if (index == widgetCount - 1) { if (index == widgetCount - 1) {
return endItemView; return endItemView;
} }
final poll = data.items[index]; final pollWithStats = data.items[index];
return _CreatorPollItem(poll: poll, pubName: pubName); return _CreatorPollItem(
pollWithStats: pollWithStats,
pubName: pubName,
);
}, },
), ),
), ),
@@ -105,14 +118,14 @@ class CreatorPollListScreen extends HookConsumerWidget {
class _CreatorPollItem extends StatelessWidget { class _CreatorPollItem extends StatelessWidget {
final String pubName; final String pubName;
const _CreatorPollItem({required this.poll, required this.pubName}); const _CreatorPollItem({required this.pollWithStats, required this.pubName});
final SnPoll poll; final SnPollWithStats pollWithStats;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final ended = poll.endedAt; final ended = pollWithStats.endedAt;
final endedText = final endedText =
ended == null ended == null
? 'No end' ? 'No end'
@@ -122,15 +135,16 @@ class _CreatorPollItem extends StatelessWidget {
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
title: Text(poll.title ?? 'Untitled poll'), title: Text(pollWithStats.title ?? 'Untitled poll'),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (poll.description != null && poll.description!.isNotEmpty) if (pollWithStats.description != null &&
pollWithStats.description!.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
child: Text( child: Text(
poll.description!, pollWithStats.description!,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -138,7 +152,7 @@ class _CreatorPollItem extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
child: Text( child: Text(
'Questions: ${poll.questions.length} · Ends: $endedText', 'Questions: ${pollWithStats.questions.length} · Ends: $endedText',
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
), ),
), ),
@@ -158,7 +172,7 @@ class _CreatorPollItem extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'creatorPollEdit', 'creatorPollEdit',
pathParameters: {'name': pubName, 'id': poll.id}, pathParameters: {'name': pubName, 'id': pollWithStats.id},
); );
}, },
), ),
@@ -169,8 +183,7 @@ class _CreatorPollItem extends StatelessWidget {
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => PollFeedbackSheet(pollId: pollWithStats.id),
(context) => PollFeedbackSheet(pollId: poll.id, poll: poll),
); );
}, },
), ),

View File

@@ -6,7 +6,7 @@ part of 'poll_list.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$pollListNotifierHash() => r'd3da24ff6bbb8f35b06d57fc41625dc0312508e4'; String _$pollWithStatsHash() => r'6bb910046ce1e09368f9922dbec52fdc2cc86740';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -29,11 +29,133 @@ class _SystemHash {
} }
} }
/// See also [pollWithStats].
@ProviderFor(pollWithStats)
const pollWithStatsProvider = PollWithStatsFamily();
/// See also [pollWithStats].
class PollWithStatsFamily extends Family<AsyncValue<SnPollWithStats>> {
/// See also [pollWithStats].
const PollWithStatsFamily();
/// See also [pollWithStats].
PollWithStatsProvider call(String id) {
return PollWithStatsProvider(id);
}
@override
PollWithStatsProvider getProviderOverride(
covariant PollWithStatsProvider provider,
) {
return call(provider.id);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'pollWithStatsProvider';
}
/// See also [pollWithStats].
class PollWithStatsProvider extends AutoDisposeFutureProvider<SnPollWithStats> {
/// See also [pollWithStats].
PollWithStatsProvider(String id)
: this._internal(
(ref) => pollWithStats(ref as PollWithStatsRef, id),
from: pollWithStatsProvider,
name: r'pollWithStatsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$pollWithStatsHash,
dependencies: PollWithStatsFamily._dependencies,
allTransitiveDependencies:
PollWithStatsFamily._allTransitiveDependencies,
id: id,
);
PollWithStatsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.id,
}) : super.internal();
final String id;
@override
Override overrideWith(
FutureOr<SnPollWithStats> Function(PollWithStatsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PollWithStatsProvider._internal(
(ref) => create(ref as PollWithStatsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<SnPollWithStats> createElement() {
return _PollWithStatsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PollWithStatsProvider && other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PollWithStatsRef on AutoDisposeFutureProviderRef<SnPollWithStats> {
/// The parameter `id` of this provider.
String get id;
}
class _PollWithStatsProviderElement
extends AutoDisposeFutureProviderElement<SnPollWithStats>
with PollWithStatsRef {
_PollWithStatsProviderElement(super.provider);
@override
String get id => (origin as PollWithStatsProvider).id;
}
String _$pollListNotifierHash() => r'd5b822e737788be8982f5cb3b501d460441930c1';
abstract class _$PollListNotifier abstract class _$PollListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPoll>> { extends
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollWithStats>> {
late final String? pubName; late final String? pubName;
FutureOr<CursorPagingData<SnPoll>> build(String? pubName); FutureOr<CursorPagingData<SnPollWithStats>> build(String? pubName);
} }
/// See also [PollListNotifier]. /// See also [PollListNotifier].
@@ -42,7 +164,7 @@ const pollListNotifierProvider = PollListNotifierFamily();
/// See also [PollListNotifier]. /// See also [PollListNotifier].
class PollListNotifierFamily class PollListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPoll>>> { extends Family<AsyncValue<CursorPagingData<SnPollWithStats>>> {
/// See also [PollListNotifier]. /// See also [PollListNotifier].
const PollListNotifierFamily(); const PollListNotifierFamily();
@@ -78,7 +200,7 @@ class PollListNotifierProvider
extends extends
AutoDisposeAsyncNotifierProviderImpl< AutoDisposeAsyncNotifierProviderImpl<
PollListNotifier, PollListNotifier,
CursorPagingData<SnPoll> CursorPagingData<SnPollWithStats>
> { > {
/// See also [PollListNotifier]. /// See also [PollListNotifier].
PollListNotifierProvider(String? pubName) PollListNotifierProvider(String? pubName)
@@ -109,7 +231,7 @@ class PollListNotifierProvider
final String? pubName; final String? pubName;
@override @override
FutureOr<CursorPagingData<SnPoll>> runNotifierBuild( FutureOr<CursorPagingData<SnPollWithStats>> runNotifierBuild(
covariant PollListNotifier notifier, covariant PollListNotifier notifier,
) { ) {
return notifier.build(pubName); return notifier.build(pubName);
@@ -134,7 +256,7 @@ class PollListNotifierProvider
@override @override
AutoDisposeAsyncNotifierProviderElement< AutoDisposeAsyncNotifierProviderElement<
PollListNotifier, PollListNotifier,
CursorPagingData<SnPoll> CursorPagingData<SnPollWithStats>
> >
createElement() { createElement() {
return _PollListNotifierProviderElement(this); return _PollListNotifierProviderElement(this);
@@ -157,7 +279,7 @@ class PollListNotifierProvider
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
mixin PollListNotifierRef mixin PollListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPoll>> { on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollWithStats>> {
/// The parameter `pubName` of this provider. /// The parameter `pubName` of this provider.
String? get pubName; String? get pubName;
} }
@@ -166,7 +288,7 @@ class _PollListNotifierProviderElement
extends extends
AutoDisposeAsyncNotifierProviderElement< AutoDisposeAsyncNotifierProviderElement<
PollListNotifier, PollListNotifier,
CursorPagingData<SnPoll> CursorPagingData<SnPollWithStats>
> >
with PollListNotifierRef { with PollListNotifierRef {
_PollListNotifierProviderElement(super.provider); _PollListNotifierProviderElement(super.provider);

View File

@@ -58,7 +58,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
try { try {
showLoadingModal(context); showLoadingModal(context);
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/stickers/$id/content/${sticker.id}'); await apiClient.delete('/sphere/stickers/$id/content/${sticker.id}');
ref.invalidate(stickerPackContentProvider(id)); ref.invalidate(stickerPackContentProvider(id));
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@@ -180,6 +180,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
.pushNamed( .pushNamed(
'creatorStickerEdit', 'creatorStickerEdit',
pathParameters: { pathParameters: {
'name': pubName,
'packId': id, 'packId': id,
'id': sticker.id, 'id': sticker.id,
}, },
@@ -297,7 +298,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
).then((confirm) { ).then((confirm) {
if (confirm) { if (confirm) {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
client.delete('/stickers/$packId'); client.delete('/sphere/stickers/$packId');
ref.invalidate(stickerPacksNotifierProvider); ref.invalidate(stickerPacksNotifierProvider);
if (context.mounted) context.pop(true); if (context.mounted) context.pop(true);
} }
@@ -325,7 +326,7 @@ Future<SnSticker?> stickerPackSticker(
if (query == null) return null; if (query == null) return null;
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get( final resp = await apiClient.get(
'/stickers/${query.packId}/content/${query.id}', '/sphere/stickers/${query.packId}/content/${query.id}',
); );
if (resp.data == null) return null; if (resp.data == null) return null;
return SnSticker.fromJson(resp.data); return SnSticker.fromJson(resp.data);
@@ -379,8 +380,8 @@ class EditStickersScreen extends HookConsumerWidget {
try { try {
final resp = await apiClient.request( final resp = await apiClient.request(
id == null id == null
? '/stickers/$packId/content' ? '/sphere/stickers/$packId/content'
: '/stickers/$packId/content/$id', : '/sphere/stickers/$packId/content/$id',
data: {'slug': slugController.text, 'image_id': imageController.text}, data: {'slug': slugController.text, 'image_id': imageController.text},
options: Options(method: id == null ? 'POST' : 'PATCH'), options: Options(method: id == null ? 'POST' : 'PATCH'),
); );

View File

@@ -151,7 +151,7 @@ class _StickerPackContentProviderElement
} }
String _$stickerPackStickerHash() => String _$stickerPackStickerHash() =>
r'36f524c047e632236d5597aaaa8678ed86599602'; r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0';
/// See also [stickerPackSticker]. /// See also [stickerPackSticker].
@ProviderFor(stickerPackSticker) @ProviderFor(stickerPackSticker)

View File

@@ -31,7 +31,7 @@ class StickersScreen extends HookConsumerWidget {
context context
.pushNamed( .pushNamed(
'creatorStickerPackNew', 'creatorStickerPackNew',
queryParameters: {'name': pubName}, pathParameters: {'name': pubName},
) )
.then((value) { .then((value) {
if (value != null) { if (value != null) {
@@ -187,10 +187,8 @@ class EditStickerPacksScreen extends HookConsumerWidget {
'description': descriptionController.text, 'description': descriptionController.text,
'prefix': prefixController.text, 'prefix': prefixController.text,
}, },
options: Options( queryParameters: {'pub': pubName},
method: packId == null ? 'POST' : 'PATCH', options: Options(method: packId == null ? 'POST' : 'PATCH'),
headers: {'X-Pub': pubName},
),
); );
if (!context.mounted) return; if (!context.mounted) return;
context.pop(SnStickerPack.fromJson(resp.data)); context.pop(SnStickerPack.fromJson(resp.data));

View File

@@ -114,10 +114,11 @@ class WebFeedEditScreen extends HookConsumerWidget {
return feedAsync.when( return feedAsync.when(
loading: loading:
() => () => const AppScaffold(
const Scaffold(body: Center(child: CircularProgressIndicator())), body: Center(child: CircularProgressIndicator()),
),
error: error:
(error, stack) => Scaffold( (error, stack) => AppScaffold(
appBar: AppBar(title: const Text('Error')), appBar: AppBar(title: const Text('Error')),
body: Center(child: Text('Error: $error')), body: Center(child: Text('Error: $error')),
), ),

View File

@@ -30,12 +30,12 @@ Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
} }
@riverpod @riverpod
Future<List<SnPublisher>> developers(Ref ref) async { Future<List<SnDeveloper>> developers(Ref ref) async {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers'); final resp = await client.get('/develop/developers');
return resp.data return resp.data
.map((e) => SnPublisher.fromJson(e)) .map((e) => SnDeveloper.fromJson(e))
.cast<SnPublisher>() .cast<SnDeveloper>()
.toList(); .toList();
} }
@@ -74,25 +74,25 @@ class DeveloperHubScreen extends HookConsumerWidget {
} }
final developers = ref.watch(developersProvider); final developers = ref.watch(developersProvider);
final currentDeveloper = useState<SnPublisher?>( final currentDeveloper = useState<SnDeveloper?>(
developers.value?.firstOrNull, developers.value?.firstOrNull,
); );
final List<DropdownMenuItem<SnPublisher>> developersMenu = developers.when( final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when(
data: data:
(data) => (data) =>
data data
.map( .map(
(item) => DropdownMenuItem<SnPublisher>( (item) => DropdownMenuItem<SnDeveloper>(
value: item, value: item,
child: ListTile( child: ListTile(
minTileHeight: 48, minTileHeight: 48,
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
radius: 16, radius: 16,
fileId: item.picture?.id, fileId: item.publisher?.picture?.id,
), ),
title: Text(item.nick), title: Text(item.publisher!.nick),
subtitle: Text('@${item.name}'), subtitle: Text('@${item.publisher!.name}'),
trailing: trailing:
currentDeveloper.value?.id == item.id currentDeveloper.value?.id == item.id
? const Icon(Icons.check) ? const Icon(Icons.check)
@@ -107,7 +107,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
); );
final developerStats = ref.watch( final developerStats = ref.watch(
developerStatsProvider(currentDeveloper.value?.name), developerStatsProvider(currentDeveloper.value?.publisher?.name),
); );
return AppScaffold( return AppScaffold(
@@ -117,7 +117,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
title: Text('developerHub').tr(), title: Text('developerHub').tr(),
actions: [ actions: [
DropdownButtonHideUnderline( DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>( child: DropdownButton2<SnDeveloper>(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
value: currentDeveloper.value, value: currentDeveloper.value,
hint: CircleAvatar( hint: CircleAvatar(
@@ -139,7 +139,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
...developersMenu.map( ...developersMenu.map(
(e) => ProfilePictureWidget( (e) => ProfilePictureWidget(
radius: 16, radius: 16,
fileId: e.value?.picture?.id, fileId: e.value?.publisher?.picture?.id,
).center().padding(right: 8), ).center().padding(right: 8),
), ),
]; ];
@@ -193,10 +193,12 @@ class DeveloperHubScreen extends HookConsumerWidget {
...(developers.value?.map( ...(developers.value?.map(
(developer) => ListTile( (developer) => ListTile(
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
file: developer.picture, file: developer.publisher?.picture,
),
title: Text(developer.publisher!.nick),
subtitle: Text(
'@${developer.publisher!.name}',
), ),
title: Text(developer.nick),
subtitle: Text('@${developer.name}'),
onTap: () { onTap: () {
currentDeveloper.value = developer; currentDeveloper.value = developer;
}, },
@@ -243,7 +245,8 @@ class DeveloperHubScreen extends HookConsumerWidget {
context.pushNamed( context.pushNamed(
'developerApps', 'developerApps',
pathParameters: { pathParameters: {
'name': currentDeveloper.value!.name, 'name':
currentDeveloper.value!.publisher!.name,
}, },
); );
}, },
@@ -257,7 +260,9 @@ class DeveloperHubScreen extends HookConsumerWidget {
error: err, error: err,
onRetry: () { onRetry: () {
ref.invalidate( ref.invalidate(
developerStatsProvider(currentDeveloper.value?.name), developerStatsProvider(
currentDeveloper.value?.publisher!.name,
),
); );
}, },
), ),
@@ -354,7 +359,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
? Center( ? Center(
child: child:
Text( Text(
'noPublishersToEnroll', 'noDevelopersToEnroll',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),
) )

View File

@@ -149,12 +149,12 @@ class _DeveloperStatsProviderElement
String? get uname => (origin as DeveloperStatsProvider).uname; String? get uname => (origin as DeveloperStatsProvider).uname;
} }
String _$developersHash() => r'04f25db31f511f651a5add128d56631236ed0b39'; String _$developersHash() => r'252341098617ac398ce133994453f318dd3edbd2';
/// See also [developers]. /// See also [developers].
@ProviderFor(developers) @ProviderFor(developers)
final developersProvider = final developersProvider =
AutoDisposeFutureProvider<List<SnPublisher>>.internal( AutoDisposeFutureProvider<List<SnDeveloper>>.internal(
developers, developers,
name: r'developersProvider', name: r'developersProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@@ -167,6 +167,6 @@ final developersProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnPublisher>>; typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.dart'; import 'package:island/route.dart';

View File

@@ -9,7 +9,9 @@ import 'package:gap/gap.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:easy_localization/easy_localization.dart';
class PollEditorState { class PollEditorState {
String? id; // for editing String? id; // for editing
@@ -109,7 +111,7 @@ class PollEditor extends Notifier<PollEditorState> {
? [ ? [
SnPollOption( SnPollOption(
id: const Uuid().v4(), id: const Uuid().v4(),
label: 'Option 1', label: 'pollOptionDefaultLabel'.tr(),
order: 0, order: 0,
), ),
] ]
@@ -190,7 +192,7 @@ class PollEditor extends Notifier<PollEditorState> {
: [ : [
SnPollOption( SnPollOption(
id: const Uuid().v4(), id: const Uuid().v4(),
label: 'Option 1', label: 'pollOptionDefaultLabel'.tr(),
order: 0, order: 0,
), ),
]) ])
@@ -388,7 +390,7 @@ class PollEditorScreen extends ConsumerWidget {
data: body, data: body,
)); ));
showSnackBar(isUpdate ? 'Poll updated.' : 'Poll created.'); showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr());
if (!context.mounted) return; if (!context.mounted) return;
Navigator.of(context).maybePop(res.data); Navigator.of(context).maybePop(res.data);
@@ -413,13 +415,13 @@ class PollEditorScreen extends ConsumerWidget {
}); });
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'), title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
actions: [ actions: [
if (kDebugMode) if (kDebugMode)
IconButton( IconButton(
tooltip: 'Preview JSON (debug)', tooltip: 'pollPreviewJsonDebug'.tr(),
onPressed: () { onPressed: () {
_showDebugPreview(context, model); _showDebugPreview(context, model);
}, },
@@ -428,175 +430,175 @@ class PollEditorScreen extends ConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
body: SafeArea( body: Column(
child: Form( children: [
key: ValueKey(model.id), Expanded(
child: ListView( child: Form(
padding: const EdgeInsets.all(16), key: ValueKey(model.id),
children: [ child: ListView(
TextFormField( padding: const EdgeInsets.all(16),
initialValue: model.title ?? '',
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
textInputAction: TextInputAction.next,
maxLength: 256,
onChanged: notifier.setTitle,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Title is required';
}
return null;
},
),
const Gap(12),
TextFormField(
initialValue: model.description ?? '',
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
maxLines: 3,
maxLength: 4096,
onChanged: notifier.setDescription,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
_EndDatePicker(
value: model.endedAt,
onChanged: notifier.setEndedAt,
),
const Gap(24),
Row(
children: [ children: [
Text( TextFormField(
'Questions', initialValue: model.title ?? '',
style: Theme.of(context).textTheme.titleLarge, decoration: InputDecoration(
), labelText: 'title'.tr(),
const Spacer(), border: OutlineInputBorder(
MenuAnchor( borderRadius: BorderRadius.all(Radius.circular(16)),
builder: (context, controller, child) { ),
return FilledButton.icon( ),
onPressed: () { textInputAction: TextInputAction.next,
controller.isOpen maxLength: 256,
? controller.close() onChanged: notifier.setTitle,
: controller.open(); onTapOutside:
}, (_) => FocusManager.instance.primaryFocus?.unfocus(),
icon: const Icon(Icons.add), validator: (v) {
label: const Text('Add question'), if (v == null || v.trim().isEmpty) {
); return 'pollTitleRequired'.tr();
}
return null;
}, },
menuChildren:
SnPollQuestionType.values
.map(
(t) => MenuItemButton(
leadingIcon: Icon(_iconForType(t)),
onPressed: () => notifier.addQuestion(t),
child: Text(_labelForType(t)),
),
)
.toList(),
), ),
const Gap(12),
TextFormField(
initialValue: model.description ?? '',
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
maxLines: 3,
maxLength: 4096,
onChanged: notifier.setDescription,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
_EndDatePicker(
value: model.endedAt,
onChanged: notifier.setEndedAt,
),
const Gap(24),
Row(
children: [
Text(
'questions'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
MenuAnchor(
builder: (context, controller, child) {
return FilledButton.icon(
onPressed: () {
controller.isOpen
? controller.close()
: controller.open();
},
icon: const Icon(Icons.add),
label: Text('pollAddQuestion'.tr()),
);
},
menuChildren:
SnPollQuestionType.values
.map(
(t) => MenuItemButton(
leadingIcon: Icon(_iconForType(t)),
onPressed: () => notifier.addQuestion(t),
child: Text(_labelForType(t)),
),
)
.toList(),
),
],
),
const Gap(8),
if (model.questions.isEmpty)
_EmptyState(
title: 'pollNoQuestionsYet'.tr(),
subtitle:
'pollNoQuestionsHint'.tr(),
)
else
ReorderableListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: model.questions.length,
onReorder: (oldIndex, newIndex) {
// Convert to stepwise moves using provided functions
if (newIndex > oldIndex) newIndex -= 1;
final steps = newIndex - oldIndex;
if (steps == 0) return;
if (steps > 0) {
for (int i = 0; i < steps; i++) {
notifier.moveQuestionDown(oldIndex + i);
}
} else {
for (int i = 0; i > steps; i--) {
notifier.moveQuestionUp(oldIndex + i);
}
}
},
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final q = model.questions[index];
return Card(
key: ValueKey('q_$index'),
margin: const EdgeInsets.symmetric(vertical: 8),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
_QuestionHeader(
index: index,
question: q,
onMoveUp:
index > 0
? () => notifier.moveQuestionUp(index)
: null,
onMoveDown:
index < model.questions.length - 1
? () => notifier.moveQuestionDown(index)
: null,
onDelete: () => notifier.removeQuestion(index),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: _QuestionEditor(
index: index,
question: q,
),
),
],
),
);
},
),
const Gap(96),
], ],
), ),
const Gap(8), ),
if (model.questions.isEmpty) ),
_EmptyState( Row(
title: 'No questions yet', children: [
subtitle: 'Use "Add question" to start building your poll.', OutlinedButton.icon(
) onPressed: () {
else Navigator.of(context).maybePop();
ReorderableListView.builder( },
shrinkWrap: true, icon: const Icon(Icons.close),
physics: const NeverScrollableScrollPhysics(), label: Text('cancel'.tr()),
itemCount: model.questions.length, ),
onReorder: (oldIndex, newIndex) { const Spacer(),
// Convert to stepwise moves using provided functions FilledButton.icon(
if (newIndex > oldIndex) newIndex -= 1; onPressed: () {
final steps = newIndex - oldIndex; _submitPoll(context, ref);
if (steps == 0) return; },
if (steps > 0) { icon: const Icon(Icons.cloud_upload_outlined),
for (int i = 0; i < steps; i++) { label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
notifier.moveQuestionDown(oldIndex + i); ),
}
} else {
for (int i = 0; i > steps; i--) {
notifier.moveQuestionUp(oldIndex + i);
}
}
},
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final q = model.questions[index];
return Card(
key: ValueKey('q_$index'),
margin: const EdgeInsets.symmetric(vertical: 8),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
_QuestionHeader(
index: index,
question: q,
onMoveUp:
index > 0
? () => notifier.moveQuestionUp(index)
: null,
onMoveDown:
index < model.questions.length - 1
? () => notifier.moveQuestionDown(index)
: null,
onDelete: () => notifier.removeQuestion(index),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: _QuestionEditor(index: index, question: q),
),
],
),
);
},
),
const Gap(96),
], ],
), ),
), ],
),
bottomNavigationBar: Padding(
padding: EdgeInsets.fromLTRB(
16,
8,
16,
16 + MediaQuery.of(context).padding.bottom,
),
child: Row(
children: [
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).maybePop();
},
icon: const Icon(Icons.close),
label: const Text('Cancel'),
),
const Spacer(),
FilledButton.icon(
onPressed: () {
_submitPoll(context, ref);
},
icon: const Icon(Icons.cloud_upload_outlined),
label: Text(model.id == null ? 'Create' : 'Update'),
),
],
),
), ),
); );
} }
@@ -636,14 +638,14 @@ class PollEditorScreen extends ConsumerWidget {
context: context, context: context,
builder: builder:
(_) => AlertDialog( (_) => AlertDialog(
title: const Text('Debug Preview'), title: Text('pollDebugPreview'.tr()),
content: SingleChildScrollView( content: SingleChildScrollView(
child: SelectableText(buf.toString()), child: SelectableText(buf.toString()),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'), child: Text('close'.tr()),
), ),
], ],
), ),
@@ -672,15 +674,15 @@ IconData _iconForType(SnPollQuestionType t) {
String _labelForType(SnPollQuestionType t) { String _labelForType(SnPollQuestionType t) {
switch (t) { switch (t) {
case SnPollQuestionType.singleChoice: case SnPollQuestionType.singleChoice:
return 'Single choice'; return 'pollQuestionTypeSingleChoice'.tr();
case SnPollQuestionType.multipleChoice: case SnPollQuestionType.multipleChoice:
return 'Multiple choice'; return 'pollQuestionTypeMultipleChoice'.tr();
case SnPollQuestionType.freeText: case SnPollQuestionType.freeText:
return 'Free text'; return 'pollQuestionTypeFreeText'.tr();
case SnPollQuestionType.yesNo: case SnPollQuestionType.yesNo:
return 'Yes / No'; return 'pollQuestionTypeYesNo'.tr();
case SnPollQuestionType.rating: case SnPollQuestionType.rating:
return 'Rating'; return 'pollQuestionTypeRating'.tr();
} }
} }
@@ -697,8 +699,8 @@ class _EndDatePicker extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: InputDecorator( child: InputDecorator(
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'End date & time (optional)', labelText: 'pollEndDateOptional'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -710,7 +712,7 @@ class _EndDatePicker extends StatelessWidget {
Icon(Icons.event, color: Theme.of(context).colorScheme.primary), Icon(Icons.event, color: Theme.of(context).colorScheme.primary),
Text( Text(
value == null value == null
? 'Not set' ? 'notSet'.tr()
: MaterialLocalizations.of( : MaterialLocalizations.of(
context, context,
).formatFullDate(value!), ).formatFullDate(value!),
@@ -758,12 +760,12 @@ class _EndDatePicker extends StatelessWidget {
); );
onChanged(dt); onChanged(dt);
}, },
child: const Text('Pick'), child: Text('pick'.tr()),
), ),
if (value != null) if (value != null)
TextButton( TextButton(
onPressed: () => onChanged(null), onPressed: () => onChanged(null),
child: const Text('Clear'), child: Text('clear'.tr()),
), ),
], ],
), ),
@@ -798,7 +800,7 @@ class _QuestionHeader extends StatelessWidget {
child: const Icon(Icons.drag_handle), child: const Icon(Icons.drag_handle),
), ),
title: Text( title: Text(
question.title.isEmpty ? 'Untitled question' : question.title, question.title.isEmpty ? 'pollUntitledQuestion'.tr() : question.title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -807,17 +809,17 @@ class _QuestionHeader extends StatelessWidget {
spacing: 4, spacing: 4,
children: [ children: [
IconButton( IconButton(
tooltip: 'Move up', tooltip: 'moveUp'.tr(),
onPressed: onMoveUp, onPressed: onMoveUp,
icon: const Icon(Icons.arrow_upward), icon: const Icon(Icons.arrow_upward),
), ),
IconButton( IconButton(
tooltip: 'Move down', tooltip: 'moveDown'.tr(),
onPressed: onMoveDown, onPressed: onMoveDown,
icon: const Icon(Icons.arrow_downward), icon: const Icon(Icons.arrow_downward),
), ),
IconButton( IconButton(
tooltip: 'Delete', tooltip: 'delete'.tr(),
onPressed: onDelete, onPressed: onDelete,
icon: const Icon(Icons.delete_outline), icon: const Icon(Icons.delete_outline),
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
@@ -852,7 +854,7 @@ class _QuestionEditor extends ConsumerWidget {
onChanged: (t) => notifier.setQuestionType(index, t), onChanged: (t) => notifier.setQuestionType(index, t),
), ),
FilterChip( FilterChip(
label: const Text('Required'), label: Text('required'.tr()),
selected: question.isRequired, selected: question.isRequired,
onSelected: (v) => notifier.setQuestionRequired(index, v), onSelected: (v) => notifier.setQuestionRequired(index, v),
avatar: Icon( avatar: Icon(
@@ -866,8 +868,8 @@ class _QuestionEditor extends ConsumerWidget {
const Gap(12), const Gap(12),
TextFormField( TextFormField(
initialValue: question.title, initialValue: question.title,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Question title', labelText: 'pollQuestionTitle'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -878,7 +880,7 @@ class _QuestionEditor extends ConsumerWidget {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
validator: (v) { validator: (v) {
if (v == null || v.trim().isEmpty) { if (v == null || v.trim().isEmpty) {
return 'Question title is required'; return 'pollQuestionTitleRequired'.tr();
} }
return null; return null;
}, },
@@ -886,8 +888,8 @@ class _QuestionEditor extends ConsumerWidget {
const Gap(12), const Gap(12),
TextFormField( TextFormField(
initialValue: question.description ?? '', initialValue: question.description ?? '',
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Question description (optional)', labelText: 'pollQuestionDescriptionOptional'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -901,7 +903,7 @@ class _QuestionEditor extends ConsumerWidget {
), ),
if (question.options != null) ...[ if (question.options != null) ...[
const Gap(16), const Gap(16),
Text('Options', style: Theme.of(context).textTheme.titleMedium), Text('options'.tr(), style: Theme.of(context).textTheme.titleMedium),
const Gap(8), const Gap(8),
_OptionsEditor(index: index, options: question.options!), _OptionsEditor(index: index, options: question.options!),
const Gap(4), const Gap(4),
@@ -910,7 +912,7 @@ class _QuestionEditor extends ConsumerWidget {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () => notifier.addOption(index), onPressed: () => notifier.addOption(index),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('Add option'), label: Text('pollAddOption'.tr()),
), ),
), ),
], ],
@@ -936,8 +938,8 @@ class _QuestionTypePicker extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DropdownButtonFormField<SnPollQuestionType>( return DropdownButtonFormField<SnPollQuestionType>(
value: value, value: value,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Type', labelText: 'Type'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -986,8 +988,8 @@ class _OptionsEditor extends ConsumerWidget {
child: TextFormField( child: TextFormField(
key: ValueKey(options[i].id), key: ValueKey(options[i].id),
initialValue: options[i].label, initialValue: options[i].label,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Option label', labelText: 'pollOptionLabel'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -1002,7 +1004,7 @@ class _OptionsEditor extends ConsumerWidget {
SizedBox( SizedBox(
width: 40, width: 40,
child: IconButton( child: IconButton(
tooltip: 'Move up', tooltip: 'moveUp'.tr(),
onPressed: onPressed:
i > 0 ? () => notifier.moveOptionUp(index, i) : null, i > 0 ? () => notifier.moveOptionUp(index, i) : null,
icon: const Icon(Icons.arrow_upward), icon: const Icon(Icons.arrow_upward),
@@ -1011,7 +1013,7 @@ class _OptionsEditor extends ConsumerWidget {
SizedBox( SizedBox(
width: 40, width: 40,
child: IconButton( child: IconButton(
tooltip: 'Move down', tooltip: 'moveDown'.tr(),
onPressed: onPressed:
i < options.length - 1 i < options.length - 1
? () => notifier.moveOptionDown(index, i) ? () => notifier.moveOptionDown(index, i)
@@ -1022,7 +1024,7 @@ class _OptionsEditor extends ConsumerWidget {
SizedBox( SizedBox(
width: 40, width: 40,
child: IconButton( child: IconButton(
tooltip: 'Delete', tooltip: 'delete'.tr(),
onPressed: () => notifier.removeOption(index, i), onPressed: () => notifier.removeOption(index, i),
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
), ),
@@ -1047,7 +1049,7 @@ class _TextAnswerPreview extends StatelessWidget {
maxLines: long ? 4 : 1, maxLines: long ? 4 : 1,
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText:
long ? 'Long text answer (preview)' : 'Short text answer (preview)', long ? 'pollLongTextAnswerPreview'.tr() : 'pollShortTextAnswerPreview'.tr(),
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -1081,9 +1083,9 @@ class _EmptyState extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: Theme.of(context).textTheme.titleMedium), Text('pollNoQuestionsYet'.tr(), style: Theme.of(context).textTheme.titleMedium),
const Gap(4), const Gap(4),
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), Text('pollNoQuestionsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium),
], ],
), ),
), ),

View File

@@ -92,6 +92,7 @@ class PostDetailScreen extends HookConsumerWidget {
right: 0, right: 0,
child: Material( child: Material(
elevation: 2, elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainer,
child: postState child: postState
.when( .when(
data: data:
@@ -107,8 +108,8 @@ class PostDetailScreen extends HookConsumerWidget {
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
) )
.padding( .padding(
bottom: MediaQuery.of(context).padding.bottom + 16, bottom: MediaQuery.of(context).padding.bottom + 8,
top: 16, top: 8,
horizontal: 16, horizontal: 16,
), ),
), ),

View File

@@ -7,7 +7,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/color.dart'; import 'package:island/services/color.dart';

View File

@@ -488,6 +488,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
Future<void> invitePerson() async { Future<void> invitePerson() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true,
context: context, context: context,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );

View File

@@ -10,7 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:island/main.dart'; import 'package:island/main.dart';
import 'package:island/route.dart'; import 'package:island/route.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/widgets/app_notification.dart'; import 'package:island/widgets/app_notification.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart'; import 'package:top_snackbar_flutter/top_snack_bar.dart';
@@ -67,6 +67,9 @@ Future<void> subscribePushNotification(
Dio apiClient, { Dio apiClient, {
bool detailedErrors = false, bool detailedErrors = false,
}) async { }) async {
if (Platform.isLinux) {
return;
}
await FirebaseMessaging.instance.requestPermission( await FirebaseMessaging.instance.requestPermission(
alert: true, alert: true,
badge: true, badge: true,

View File

@@ -1,19 +1,28 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app_update/azhon_app_update.dart';
import 'package:flutter_app_update/update_model.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:collection/collection.dart'; // Added for firstWhereOrNull
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
/// Data model for a GitHub release we care about /// Data model for a GitHub release we care about
class GithubReleaseInfo { class GithubReleaseInfo {
final String tagName; // e.g. 3.1.0+118 final String tagName;
final String name; // release title final String name;
final String body; // changelog markdown final String body;
final String htmlUrl; // release page final String htmlUrl;
final DateTime createdAt; final DateTime createdAt;
final List<GithubReleaseAsset> assets;
const GithubReleaseInfo({ const GithubReleaseInfo({
required this.tagName, required this.tagName,
@@ -21,9 +30,28 @@ class GithubReleaseInfo {
required this.body, required this.body,
required this.htmlUrl, required this.htmlUrl,
required this.createdAt, required this.createdAt,
this.assets = const [],
}); });
} }
/// Data model for a GitHub release asset
class GithubReleaseAsset {
final String name;
final String browserDownloadUrl;
const GithubReleaseAsset({
required this.name,
required this.browserDownloadUrl,
});
factory GithubReleaseAsset.fromJson(Map<String, dynamic> json) {
return GithubReleaseAsset(
name: json['name'] as String,
browserDownloadUrl: json['browser_download_url'] as String,
);
}
}
/// Parses version and build number from "x.y.z+build" /// Parses version and build number from "x.y.z+build"
class _ParsedVersion implements Comparable<_ParsedVersion> { class _ParsedVersion implements Comparable<_ParsedVersion> {
final int major; final int major;
@@ -62,7 +90,7 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
} }
class UpdateService { class UpdateService {
UpdateService({Dio? dio}) UpdateService({Dio? dio, this.useProxy = false})
: _dio = : _dio =
dio ?? dio ??
Dio( Dio(
@@ -78,6 +106,9 @@ class UpdateService {
); );
final Dio _dio; final Dio _dio;
final bool useProxy;
static const _proxyBaseUrl = 'https://ghfast.top/';
static const _releasesLatestApi = static const _releasesLatestApi =
'https://api.github.com/repos/solsynth/solian/releases/latest'; 'https://api.github.com/repos/solsynth/solian/releases/latest';
@@ -85,31 +116,52 @@ class UpdateService {
/// Checks GitHub for the latest release and compares against the current app version. /// Checks GitHub for the latest release and compares against the current app version.
/// If update is available, shows a bottom sheet with changelog and an action to open release page. /// If update is available, shows a bottom sheet with changelog and an action to open release page.
Future<void> checkForUpdates(BuildContext context) async { Future<void> checkForUpdates(BuildContext context) async {
log('[Update] Checking for updates...');
try { try {
final release = await fetchLatestRelease(); final release = await fetchLatestRelease();
if (release == null) return; if (release == null) {
log('[Update] No latest release found or could not fetch.');
return;
}
log('[Update] Fetched latest release: ${release.tagName}');
final info = await PackageInfo.fromPlatform(); final info = await PackageInfo.fromPlatform();
final localVersionStr = '${info.version}+${info.buildNumber}'; final localVersionStr = '${info.version}+${info.buildNumber}';
log('[Update] Local app version: $localVersionStr');
final latest = _ParsedVersion.tryParse(release.tagName); final latest = _ParsedVersion.tryParse(release.tagName);
final local = _ParsedVersion.tryParse(localVersionStr); final local = _ParsedVersion.tryParse(localVersionStr);
if (latest == null || local == null) { if (latest == null || local == null) {
log(
'[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr',
);
// If parsing fails, do nothing silently // If parsing fails, do nothing silently
return; return;
} }
log('[Update] Parsed versions. Latest: $latest, Local: $local');
final needsUpdate = latest.compareTo(local) > 0; final needsUpdate = latest.compareTo(local) > 0;
if (!needsUpdate) return; if (!needsUpdate) {
log('[Update] App is up to date. No update needed.');
return;
}
log('[Update] Update available! Latest: $latest, Local: $local');
if (!context.mounted) return; if (!context.mounted) {
log('[Update] Context not mounted, cannot show update sheet.');
return;
}
// Delay to ensure UI is ready (if called at startup) // Delay to ensure UI is ready (if called at startup)
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await showUpdateSheet(context, release); if (context.mounted) {
} catch (_) { await showUpdateSheet(context, release);
log('[Update] Update sheet shown.');
}
} catch (e) {
log('[Update] Error checking for updates: $e');
// Ignore errors (network, api, etc.) // Ignore errors (network, api, etc.)
return; return;
} }
@@ -126,25 +178,68 @@ class UpdateService {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true, useRootNavigator: true,
builder: builder: (ctx) {
(ctx) => _UpdateSheet( String? androidUpdateUrl;
release: release, if (Platform.isAndroid) {
onOpen: () async { androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
final uri = Uri.parse(release.htmlUrl); }
if (await canLaunchUrl(uri)) { return _UpdateSheet(
await launchUrl(uri, mode: LaunchMode.externalApplication); release: release,
} onOpen: () async {
}, final uri = Uri.parse(release.htmlUrl);
), if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
androidUpdateUrl: androidUpdateUrl,
useProxy: useProxy, // Pass the useProxy flag
);
},
); );
} }
String? _getAndroidUpdateUrl(List<GithubReleaseAsset> assets) {
final arm64 = assets.firstWhereOrNull(
(asset) => asset.name == 'app-arm64-v8a-release.apk',
);
final armeabi = assets.firstWhereOrNull(
(asset) => asset.name == 'app-armeabi-v7a-release.apk',
);
final x86_64 = assets.firstWhereOrNull(
(asset) => asset.name == 'app-x86_64-release.apk',
);
// Prioritize arm64, then armeabi, then x86_64
if (arm64 != null) {
return arm64.browserDownloadUrl;
} else if (armeabi != null) {
return armeabi.browserDownloadUrl;
} else if (x86_64 != null) {
return x86_64.browserDownloadUrl;
}
return null;
}
/// Fetch the latest release info from GitHub. /// Fetch the latest release info from GitHub.
/// Public so other screens (e.g., About) can manually trigger update checks. /// Public so other screens (e.g., About) can manually trigger update checks.
Future<GithubReleaseInfo?> fetchLatestRelease() async { Future<GithubReleaseInfo?> fetchLatestRelease() async {
final resp = await _dio.get(_releasesLatestApi); final apiEndpoint =
if (resp.statusCode != 200) return null; useProxy
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
: _releasesLatestApi;
log(
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
);
final resp = await _dio.get(apiEndpoint);
if (resp.statusCode != 200) {
log(
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
);
return null;
}
final data = resp.data as Map<String, dynamic>; final data = resp.data as Map<String, dynamic>;
log('[Update] Successfully fetched release data.');
final tagName = (data['tag_name'] ?? '').toString(); final tagName = (data['tag_name'] ?? '').toString();
final name = (data['name'] ?? tagName).toString(); final name = (data['name'] ?? tagName).toString();
@@ -152,25 +247,70 @@ class UpdateService {
final htmlUrl = (data['html_url'] ?? '').toString(); final htmlUrl = (data['html_url'] ?? '').toString();
final createdAtStr = (data['created_at'] ?? '').toString(); final createdAtStr = (data['created_at'] ?? '').toString();
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now(); final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
final assetsData =
(data['assets'] as List<dynamic>?)
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
if (tagName.isEmpty || htmlUrl.isEmpty) return null; if (tagName.isEmpty || htmlUrl.isEmpty) {
log(
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
);
return null;
}
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
return GithubReleaseInfo( return GithubReleaseInfo(
tagName: tagName, tagName: tagName,
name: name, name: name,
body: body, body: body,
htmlUrl: htmlUrl, htmlUrl: htmlUrl,
createdAt: createdAt, createdAt: createdAt,
assets: assetsData,
); );
} }
} }
class _UpdateSheet extends StatelessWidget { class _UpdateSheet extends StatefulWidget {
const _UpdateSheet({required this.release, required this.onOpen}); const _UpdateSheet({
required this.release,
required this.onOpen,
this.androidUpdateUrl,
this.useProxy = false,
});
final String? androidUpdateUrl;
final bool useProxy;
final GithubReleaseInfo release; final GithubReleaseInfo release;
final VoidCallback onOpen; final VoidCallback onOpen;
@override
State<_UpdateSheet> createState() => _UpdateSheetState();
}
class _UpdateSheetState extends State<_UpdateSheet> {
late bool _useProxy;
@override
void initState() {
super.initState();
_useProxy = widget.useProxy;
}
Future<void> _installUpdate(String url) async {
final downloadUrl =
_useProxy ? 'https://ghfast.top/${Uri.encodeComponent(url)}' : url;
UpdateModel model = UpdateModel(
downloadUrl,
"solian-update-${widget.release.tagName}.apk",
"launcher_icon",
'https://apps.apple.com/us/app/solian/id6499032345',
);
AzhonAppUpdate.update(model);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -186,8 +326,11 @@ class _UpdateSheet extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(release.name, style: theme.textTheme.titleMedium).bold(), Text(
Text(release.tagName).fontSize(12), widget.release.name,
style: theme.textTheme.titleMedium,
).bold(),
Text(widget.release.tagName).fontSize(12),
], ],
).padding(vertical: 16, horizontal: 16), ).padding(vertical: 16, horizontal: 16),
const Divider(height: 1), const Divider(height: 1),
@@ -197,21 +340,45 @@ class _UpdateSheet extends StatelessWidget {
horizontal: 16, horizontal: 16,
vertical: 16, vertical: 16,
), ),
child: SelectableText( child: MarkdownTextContent(
release.body.isEmpty content:
? 'No changelog provided.' widget.release.body.isEmpty
: release.body, ? 'No changelog provided.'
style: theme.textTheme.bodyMedium, : widget.release.body,
), ),
), ),
), ),
if (!kIsWeb && Platform.isAndroid)
SwitchListTile(
title: const Text('Use GitHub Proxy for Download'),
value: _useProxy,
onChanged: (value) {
setState(() {
_useProxy = value;
});
},
).padding(horizontal: 8),
Column( Column(
children: [ children: [
Row( Row(
spacing: 8,
children: [ children: [
if (!kIsWeb &&
Platform.isAndroid &&
widget.androidUpdateUrl != null)
Expanded(
child: FilledButton.icon(
onPressed: () {
log(widget.androidUpdateUrl!);
_installUpdate(widget.androidUpdateUrl!);
},
icon: const Icon(Symbols.update),
label: const Text('Install update'),
),
),
Expanded( Expanded(
child: FilledButton.icon( child: FilledButton.icon(
onPressed: onOpen, onPressed: widget.onOpen,
icon: const Icon(Icons.open_in_new), icon: const Icon(Icons.open_in_new),
label: const Text('Open release page'), label: const Text('Open release page'),
), ),

View File

@@ -3,33 +3,34 @@ import 'dart:convert';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/services/udid.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'account_session_sheet.g.dart'; part 'account_devices.g.dart';
@riverpod @riverpod
Future<List<SnAuthDevice>> authDevices(Ref ref) async { Future<List<SnAuthDeviceWithChallenge>> authDevices(Ref ref) async {
final resp = await ref final resp = await ref
.watch(apiClientProvider) .watch(apiClientProvider)
.get('/id/accounts/me/devices'); .get('/id/accounts/me/devices');
final sessionId = resp.headers.value('x-auth-session'); final currentId = await getUdid();
final data = final data =
resp.data.map<SnAuthDevice>((e) { resp.data.map<SnAuthDeviceWithChallenge>((e) {
final ele = SnAuthDevice.fromJson(e); final ele = SnAuthDeviceWithChallenge.fromJson(e);
return ele.copyWith(isCurrent: ele.sessions.first.id == sessionId); return ele.copyWith(isCurrent: ele.deviceId == currentId);
}).toList(); }).toList();
return data; return data;
} }
class _DeviceListTile extends StatelessWidget { class _DeviceListTile extends StatelessWidget {
final SnAuthDevice device; final SnAuthDeviceWithChallenge device;
final Function(String) updateDeviceLabel; final Function(String) updateDeviceLabel;
final Function(String) logoutDevice; final Function(String) logoutDevice;
@@ -57,17 +58,16 @@ class _DeviceListTile extends StatelessWidget {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text('authSessionsCount'.plural(device.sessions.length)),
Text( Text(
'lastActiveAt'.tr( 'lastActiveAt'.tr(
args: [ args: [
DateFormat().format( DateFormat().format(
device.sessions.first.lastGrantedAt.toLocal(), device.challenges.first.createdAt.toLocal(),
), ),
], ],
), ),
), ),
Text(device.sessions.first.challenge.ipAddress), Text(device.challenges.first.ipAddress),
if (device.isCurrent) if (device.isCurrent)
Row( Row(
children: [ children: [
@@ -84,7 +84,7 @@ class _DeviceListTile extends StatelessWidget {
).padding(top: 4), ).padding(top: 4),
], ],
), ),
title: Text(device.label ?? device.sessions.first.challenge.userAgent), title: Text(device.deviceLabel ?? device.deviceName),
trailing: trailing:
isWideScreen(context) isWideScreen(context)
? Row( ? Row(
@@ -93,14 +93,13 @@ class _DeviceListTile extends StatelessWidget {
IconButton( IconButton(
icon: Icon(Icons.edit), icon: Icon(Icons.edit),
tooltip: 'authDeviceEditLabel'.tr(), tooltip: 'authDeviceEditLabel'.tr(),
onPressed: onPressed: () => updateDeviceLabel(device.deviceId),
() => updateDeviceLabel(device.sessions.first.id),
), ),
if (!device.isCurrent) if (!device.isCurrent)
IconButton( IconButton(
icon: Icon(Icons.logout), icon: Icon(Icons.logout),
tooltip: 'authDeviceLogout'.tr(), tooltip: 'authDeviceLogout'.tr(),
onPressed: () => logoutDevice(device.sessions.first.id), onPressed: () => logoutDevice(device.deviceId),
), ),
], ],
) )
@@ -124,7 +123,7 @@ class AccountSessionSheet extends HookConsumerWidget {
if (!confirm || !context.mounted) return; if (!confirm || !context.mounted) return;
try { try {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/id/accounts/me/sessions/$sessionId'); await apiClient.delete('/id/accounts/me/devices/$sessionId');
ref.invalidate(authDevicesProvider); ref.invalidate(authDevicesProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@@ -163,7 +162,7 @@ class AccountSessionSheet extends HookConsumerWidget {
try { try {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
await apiClient.patch( await apiClient.patch(
'/accounts/me/sessions/$sessionId/label', '/accounts/me/devices/$sessionId/label',
data: jsonEncode(label), data: jsonEncode(label),
); );
ref.invalidate(authDevicesProvider); ref.invalidate(authDevicesProvider);
@@ -194,7 +193,7 @@ class AccountSessionSheet extends HookConsumerWidget {
); );
} else { } else {
return Dismissible( return Dismissible(
key: Key('device-${device.sessions.first.id}'), key: Key('device-${device.id}'),
direction: direction:
device.isCurrent device.isCurrent
? DismissDirection.startToEnd ? DismissDirection.startToEnd
@@ -213,7 +212,7 @@ class AccountSessionSheet extends HookConsumerWidget {
), ),
confirmDismiss: (direction) async { confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) { if (direction == DismissDirection.startToEnd) {
updateDeviceLabel(device.sessions.first.id); updateDeviceLabel(device.deviceId);
return false; return false;
} else { } else {
final confirm = await showConfirmAlert( final confirm = await showConfirmAlert(
@@ -221,7 +220,7 @@ class AccountSessionSheet extends HookConsumerWidget {
'authDeviceLogout'.tr(), 'authDeviceLogout'.tr(),
); );
if (confirm && context.mounted) { if (confirm && context.mounted) {
logoutDevice(device.sessions.first.id); logoutDevice(device.deviceId);
} }
return false; // Don't dismiss return false; // Don't dismiss
} }

View File

@@ -1,17 +1,17 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account_session_sheet.dart'; part of 'account_devices.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$authDevicesHash() => r'8bc41a1ffc37df8e757c977b4ddae11db8faaeb5'; String _$authDevicesHash() => r'feb19238f759921e51c888f8b443a3d7761e68da';
/// See also [authDevices]. /// See also [authDevices].
@ProviderFor(authDevices) @ProviderFor(authDevices)
final authDevicesProvider = final authDevicesProvider =
AutoDisposeFutureProvider<List<SnAuthDevice>>.internal( AutoDisposeFutureProvider<List<SnAuthDeviceWithChallenge>>.internal(
authDevices, authDevices,
name: r'authDevicesProvider', name: r'authDevicesProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@@ -24,6 +24,7 @@ final authDevicesProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef AuthDevicesRef = AutoDisposeFutureProviderRef<List<SnAuthDevice>>; typedef AuthDevicesRef =
AutoDisposeFutureProviderRef<List<SnAuthDeviceWithChallenge>>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/models/wallet.dart'; import 'package:island/models/wallet.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';

View File

@@ -1,9 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -44,9 +45,8 @@ class AccountPickerSheet extends HookConsumerWidget {
} }
return Container( return Container(
constraints: BoxConstraints( padding: MediaQuery.of(context).viewInsets,
maxHeight: MediaQuery.of(context).size.height * 0.4, height: MediaQuery.of(context).size.height * 0.6,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@@ -54,8 +54,8 @@ class AccountPickerSheet extends HookConsumerWidget {
child: TextField( child: TextField(
controller: searchController, controller: searchController,
onChanged: onSearchChanged, onChanged: onSearchChanged,
decoration: const InputDecoration( decoration: InputDecoration(
hintText: 'Search accounts...', hintText: 'searchAccounts'.tr(),
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
horizontal: 18, horizontal: 18,
vertical: 16, vertical: 16,

View File

@@ -1,6 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/models/badge.dart'; import 'package:island/models/badge.dart';
class BadgeList extends StatelessWidget { class BadgeList extends StatelessWidget {

View File

@@ -2,7 +2,7 @@ import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/account/profile.dart'; import 'package:island/screens/account/profile.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/account/status.dart';
@@ -55,7 +55,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
'attitude': attitude.value, 'attitude': attitude.value,
'is_invisible': isInvisible.value, 'is_invisible': isInvisible.value,
'is_not_disturb': isNotDisturb.value, 'is_not_disturb': isNotDisturb.value,
'cleared_at': clearedAt.value?.toIso8601String(), 'cleared_at': clearedAt.value?.toUtc().toIso8601String(),
if (labelController.text.isNotEmpty) 'label': labelController.text, if (labelController.text.isNotEmpty) 'label': labelController.text,
}, },
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'), options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),

View File

@@ -69,7 +69,7 @@ void showLoadingModal(BuildContext context) {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
CircularProgressIndicator(year2023: true), CircularProgressIndicator(year2023: false),
const Gap(24), const Gap(24),
Text('loading'.tr()), Text('loading'.tr()),
], ],

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/account.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';

View File

@@ -331,7 +331,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
final websocketState = ref.watch(websocketStateProvider); final websocketState = ref.watch(websocketStateProvider);
final indicatorHeight = final indicatorHeight =
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 20); MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 25);
Color indicatorColor; Color indicatorColor;
String indicatorText; String indicatorText;

View File

@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/services/notify.dart'; import 'package:island/services/notify.dart';
import 'package:island/services/sharing_intent.dart'; import 'package:island/services/sharing_intent.dart';
import 'package:island/services/update_service.dart';
import 'package:island/widgets/content/network_status_sheet.dart'; import 'package:island/widgets/content/network_status_sheet.dart';
import 'package:island/widgets/tour/tour.dart'; import 'package:island/widgets/tour/tour.dart';
@@ -21,6 +22,7 @@ class AppWrapper extends HookConsumerWidget {
}); });
final sharingService = SharingIntentService(); final sharingService = SharingIntentService();
sharingService.initialize(context); sharingService.initialize(context);
UpdateService().checkForUpdates(context);
return () { return () {
sharingService.dispose(); sharingService.dispose();
ntySubs?.cancel(); ntySubs?.cancel();

View File

@@ -31,6 +31,7 @@ class CloudFileList extends HookConsumerWidget {
final bool disableZoomIn; final bool disableZoomIn;
final bool disableConstraint; final bool disableConstraint;
final EdgeInsets? padding; final EdgeInsets? padding;
final bool isColumn;
const CloudFileList({ const CloudFileList({
super.key, super.key,
required this.files, required this.files,
@@ -40,6 +41,7 @@ class CloudFileList extends HookConsumerWidget {
this.disableZoomIn = false, this.disableZoomIn = false,
this.disableConstraint = false, this.disableConstraint = false,
this.padding, this.padding,
this.isColumn = false,
}); });
double calculateAspectRatio() { double calculateAspectRatio() {
@@ -63,6 +65,74 @@ class CloudFileList extends HookConsumerWidget {
); );
if (files.isEmpty) return const SizedBox.shrink(); if (files.isEmpty) return const SizedBox.shrink();
if (isColumn) {
final children = <Widget>[];
const maxFiles = 2;
final filesToShow = files.take(maxFiles).toList();
for (var i = 0; i < filesToShow.length; i++) {
final file = filesToShow[i];
final isImage = file.mimeType?.startsWith('image') ?? false;
final isAudio = file.mimeType?.startsWith('audio') ?? false;
final widgetItem = ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: _CloudFileListEntry(
file: file,
heroTag: heroTags[i],
isImage: isImage,
disableZoomIn: disableZoomIn,
onTap: () {
if (!isImage) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: file, heroTag: heroTags[i]),
rootNavigator: true,
);
}
},
),
);
Widget item;
if (isAudio) {
item = SizedBox(height: 120, child: widgetItem);
} else {
item = AspectRatio(
aspectRatio: file.fileMeta?['ratio'] as double? ?? 1.0,
child: widgetItem,
);
}
children.add(item);
if (i < filesToShow.length - 1) {
children.add(const Gap(8));
}
}
if (files.length > maxFiles) {
children.add(const Gap(8));
children.add(
Text(
'filesListAdditional'.plural(files.length - filesToShow.length),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
);
}
return Padding(
padding: padding ?? EdgeInsets.zero,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
);
}
if (files.length == 1) { if (files.length == 1) {
final isImage = files.first.mimeType?.startsWith('image') ?? false; final isImage = files.first.mimeType?.startsWith('image') ?? false;
final isAudio = files.first.mimeType?.startsWith('audio') ?? false; final isAudio = files.first.mimeType?.startsWith('audio') ?? false;

View File

@@ -142,7 +142,7 @@ class CloudVideoWidget extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( Wrap(
spacing: 8, spacing: 8,
children: [ children: [
if (item.fileMeta?['duration'] != null) if (item.fileMeta?['duration'] != null)
@@ -199,8 +199,8 @@ class CloudVideoWidget extends HookConsumerWidget {
), ),
), ),
], ],
), ).padding(horizontal: 16, bottom: 12),
).padding(horizontal: 16, bottom: 12), ),
], ],
), ),
onTap: () { onTap: () {

View File

@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_highlight/themes/a11y-dark.dart'; import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart'; import 'package:flutter_highlight/themes/a11y-light.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
@@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget {
textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!, textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
), ),
HrConfig(height: 1, color: Theme.of(context).dividerColor), HrConfig(height: 1, color: Theme.of(context).dividerColor),
PreConfig(theme: isDark ? a11yDarkTheme : a11yLightTheme), PreConfig(
theme: isDark ? a11yDarkTheme : a11yLightTheme,
textStyle: GoogleFonts.robotoMono(fontSize: 14),
styleNotMatched: GoogleFonts.robotoMono(fontSize: 14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
),
TableConfig(
wrapper:
(child) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: child,
),
),
LinkConfig( LinkConfig(
style: style:
linkStyle ?? linkStyle ??
@@ -160,7 +176,7 @@ class MarkdownTextContent extends HookConsumerWidget {
uri: stickerUri, uri: stickerUri,
width: size, width: size,
height: size, height: size,
fit: BoxFit.cover, fit: BoxFit.contain,
noCacheOptimization: true, noCacheOptimization: true,
), ),
), ),

View File

@@ -248,7 +248,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
try { try {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final response = await client.post( final response = await client.post(
'/orders/${widget.order.id}/pay', '/id/orders/${widget.order.id}/pay',
data: {'pin_code': pin}, data: {'pin_code': pin},
); );

View File

@@ -1,9 +1,14 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/creators/poll/poll_list.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/poll/poll_stats_widget.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -52,59 +57,60 @@ class PollFeedbackNotifier extends _$PollFeedbackNotifier
class PollFeedbackSheet extends HookConsumerWidget { class PollFeedbackSheet extends HookConsumerWidget {
final String pollId; final String pollId;
final String? title; final String? title;
final SnPoll poll; const PollFeedbackSheet({super.key, required this.pollId, this.title});
final Map<String, dynamic>? stats; // stats object similar to PollSubmit
const PollFeedbackSheet({
super.key,
required this.pollId,
required this.poll,
this.title,
this.stats,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final poll = ref.watch(pollWithStatsProvider(pollId));
return SheetScaffold( return SheetScaffold(
titleText: title ?? 'Poll feedback', titleText: title ?? 'Poll feedback',
child: Column( child: poll.when(
crossAxisAlignment: CrossAxisAlignment.stretch, data:
children: [ (data) => CustomScrollView(
_PollHeader(poll: poll, stats: stats), slivers: [
const Divider(height: 1), SliverToBoxAdapter(child: _PollHeader(poll: data)),
Expanded( SliverToBoxAdapter(child: const Divider(height: 1)),
child: PagingHelperView( SliverGap(4),
provider: pollFeedbackNotifierProvider(pollId), PagingHelperSliverView(
futureRefreshable: pollFeedbackNotifierProvider(pollId).future, provider: pollFeedbackNotifierProvider(pollId),
notifierRefreshable: futureRefreshable:
pollFeedbackNotifierProvider(pollId).notifier, pollFeedbackNotifierProvider(pollId).future,
contentBuilder: notifierRefreshable:
(data, widgetCount, endItemView) => ListView.separated( pollFeedbackNotifierProvider(pollId).notifier,
padding: const EdgeInsets.symmetric(vertical: 4), contentBuilder:
itemCount: widgetCount, (val, widgetCount, endItemView) => SliverList.separated(
itemBuilder: (context, index) { itemCount: widgetCount,
if (index == widgetCount - 1) { itemBuilder: (context, index) {
// Provided by PagingHelperView to indicate end/loading if (index == widgetCount - 1) {
return endItemView; // Provided by PagingHelperView to indicate end/loading
} return endItemView;
final answer = data.items[index]; }
return _PollAnswerTile(answer: answer, poll: poll); final answer = val.items[index];
}, return _PollAnswerTile(answer: answer, poll: data);
separatorBuilder: },
(context, index) => separatorBuilder:
const Divider(height: 1).padding(vertical: 4), (context, index) =>
), const Divider(height: 1).padding(vertical: 4),
),
),
SliverGap(4 + MediaQuery.of(context).padding.bottom),
],
), ),
), error:
], (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(pollWithStatsProvider(pollId)),
),
loading: () => ResponseLoadingWidget(),
), ),
); );
} }
} }
class _PollHeader extends StatelessWidget { class _PollHeader extends StatelessWidget {
const _PollHeader({required this.poll, this.stats}); const _PollHeader({required this.poll});
final SnPoll poll; final SnPollWithStats poll;
final Map<String, dynamic>? stats;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -112,18 +118,32 @@ class _PollHeader extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [ children: [
if (poll.title != null) if (poll.title != null || (poll.description?.isNotEmpty ?? false))
Text(poll.title!, style: theme.textTheme.titleLarge), Column(
if (poll.description != null) crossAxisAlignment: CrossAxisAlignment.start,
Padding( children: [
padding: const EdgeInsets.only(top: 2), if (poll.title != null)
child: Text( Text(poll.title!, style: theme.textTheme.titleLarge),
poll.description!, if (poll.description?.isNotEmpty ?? false)
style: theme.textTheme.bodyMedium?.copyWith( Text(
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), poll.description!,
), style: theme.textTheme.bodyMedium?.copyWith(
), color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
],
),
Text('pollQuestions').tr().fontSize(17).bold(),
for (final q in poll.questions)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (q.title.isNotEmpty) Text(q.title).bold(),
if (q.description?.isNotEmpty ?? false) Text(q.description!),
PollStatsWidget(question: q, stats: poll.stats),
],
), ),
], ],
).padding(horizontal: 20, vertical: 16); ).padding(horizontal: 20, vertical: 16);
@@ -132,7 +152,7 @@ class _PollHeader extends StatelessWidget {
class _PollAnswerTile extends StatelessWidget { class _PollAnswerTile extends StatelessWidget {
final SnPollAnswer answer; final SnPollAnswer answer;
final SnPoll poll; final SnPollWithStats poll;
const _PollAnswerTile({required this.answer, required this.poll}); const _PollAnswerTile({required this.answer, required this.poll});
String _formatPerQuestionAnswer( String _formatPerQuestionAnswer(

View File

@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:island/models/poll.dart';
class PollStatsWidget extends StatelessWidget {
const PollStatsWidget({
super.key,
required this.question,
required this.stats,
});
final SnPollQuestion question;
final Map<String, dynamic>? stats;
@override
Widget build(BuildContext context) {
if (stats == null) return const SizedBox.shrink();
final raw = stats![question.id];
if (raw == null) return const SizedBox.shrink();
Widget? body;
switch (question.type) {
case SnPollQuestionType.rating:
// rating: avg score (double or int)
final avg = (raw['rating'] as num?)?.toDouble();
if (avg == null) break;
final theme = Theme.of(context);
body = Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(Icons.star, color: Colors.amber.shade600, size: 18),
const SizedBox(width: 6),
Text(
avg.toStringAsFixed(1),
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
break;
case SnPollQuestionType.yesNo:
// yes/no: map {true: count, false: count}
if (raw is Map) {
final int yes =
(raw[true] is int)
? raw[true] as int
: int.tryParse('${raw[true]}') ?? 0;
final int no =
(raw[false] is int)
? raw[false] as int
: int.tryParse('${raw[false]}') ?? 0;
final total = (yes + no).clamp(0, 1 << 31);
final yesPct = total == 0 ? 0.0 : yes / total;
final noPct = total == 0 ? 0.0 : no / total;
final theme = Theme.of(context);
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_BarStatRow(
label: 'Yes',
count: yes,
fraction: yesPct,
color: Colors.green.shade600,
),
const SizedBox(height: 6),
_BarStatRow(
label: 'No',
count: no,
fraction: noPct,
color: Colors.red.shade600,
),
const SizedBox(height: 4),
Text(
'Total: $total',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.singleChoice:
case SnPollQuestionType.multipleChoice:
// map optionId -> count
if (raw is Map) {
final options = [...?question.options]
..sort((a, b) => a.order.compareTo(b.order));
final List<_OptionCount> items = [];
int total = 0;
for (final opt in options) {
final dynamic v = raw[opt.id];
final int count = v is int ? v : int.tryParse('$v') ?? 0;
total += count;
items.add(_OptionCount(id: opt.id, label: opt.label, count: count));
}
if (items.isNotEmpty) {
items.sort(
(a, b) => b.count.compareTo(a.count),
); // show highest first
}
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final it in items)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: _BarStatRow(
label: it.label,
count: it.count,
fraction: total == 0 ? 0 : it.count / total,
),
),
if (items.isNotEmpty)
Text(
'Total: $total',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.freeText:
// No stats
break;
}
if (body == null) return Text('No stats available');
return Padding(
padding: const EdgeInsets.only(top: 8),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Stats',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
body,
],
),
),
),
);
}
}
class _OptionCount {
final String id;
final String label;
final int count;
const _OptionCount({
required this.id,
required this.label,
required this.count,
});
}
class _BarStatRow extends StatelessWidget {
const _BarStatRow({
required this.label,
required this.count,
required this.fraction,
this.color,
});
final String label;
final int count;
final double fraction;
final Color? color;
@override
Widget build(BuildContext context) {
final barColor = color ?? Theme.of(context).colorScheme.primary;
final bgColor = Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.6);
final fg =
(fraction.isNaN || fraction.isInfinite)
? 0.0
: fraction.clamp(0.0, 1.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label · $count', style: Theme.of(context).textTheme.labelMedium),
const SizedBox(height: 4),
LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final filled = width * fg;
return Stack(
children: [
Container(
height: 8,
width: width,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(999),
),
),
Container(
height: 8,
width: filled,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(999),
),
),
],
);
},
),
],
);
}
}

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/poll/poll_stats_widget.dart';
class PollSubmit extends ConsumerStatefulWidget { class PollSubmit extends ConsumerStatefulWidget {
const PollSubmit({ const PollSubmit({
@@ -14,6 +16,7 @@ class PollSubmit extends ConsumerStatefulWidget {
this.initialAnswers, this.initialAnswers,
this.onCancel, this.onCancel,
this.showProgress = true, this.showProgress = true,
this.isReadonly = false,
}); });
final SnPollWithStats poll; final SnPollWithStats poll;
@@ -31,6 +34,8 @@ class PollSubmit extends ConsumerStatefulWidget {
/// Whether to show a progress indicator (e.g., "2 / N"). /// Whether to show a progress indicator (e.g., "2 / N").
final bool showProgress; final bool showProgress;
final bool isReadonly;
@override @override
ConsumerState<PollSubmit> createState() => _PollSubmitState(); ConsumerState<PollSubmit> createState() => _PollSubmitState();
} }
@@ -39,6 +44,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
late final List<SnPollQuestion> _questions; late final List<SnPollQuestion> _questions;
int _index = 0; int _index = 0;
bool _submitting = false; bool _submitting = false;
bool _isModifying = false; // New state to track if user is modifying answers
/// Collected answers, keyed by questionId /// Collected answers, keyed by questionId
late Map<String, dynamic> _answers; late Map<String, dynamic> _answers;
@@ -59,7 +65,14 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
_questions = [...widget.poll.questions] _questions = [...widget.poll.questions]
..sort((a, b) => a.order.compareTo(b.order)); ..sort((a, b) => a.order.compareTo(b.order));
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {}); _answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
_loadCurrentIntoLocalState(); if (!widget.isReadonly) {
_loadCurrentIntoLocalState();
// If initial answers are provided, set _isModifying to false initially
// so the "Modify" button is shown.
if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) {
_isModifying = false;
}
}
} }
@override @override
@@ -74,7 +87,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
[...widget.poll.questions] [...widget.poll.questions]
..sort((a, b) => a.order.compareTo(b.order)), ..sort((a, b) => a.order.compareTo(b.order)),
); );
_loadCurrentIntoLocalState(); if (!widget.isReadonly) {
_loadCurrentIntoLocalState();
// If poll ID changes, reset modification state
_isModifying = false;
}
} }
} }
@@ -196,7 +213,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
// Only call onSubmit after server accepts // Only call onSubmit after server accepts
widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers)); widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers));
showSnackBar('Poll answer has been submitted.'); showSnackBar('pollAnswerSubmitted'.tr());
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
} catch (e) { } catch (e) {
showErrorAlert(e); showErrorAlert(e);
@@ -268,7 +285,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
], ],
), ),
), ),
if (widget.showProgress) if (widget.showProgress &&
_isModifying) // Only show progress when modifying
Text( Text(
'${_index + 1} / ${_questions.length}', '${_index + 1} / ${_questions.length}',
style: Theme.of(context).textTheme.labelMedium, style: Theme.of(context).textTheme.labelMedium,
@@ -310,154 +328,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
} }
Widget _buildStats(BuildContext context, SnPollQuestion q) { Widget _buildStats(BuildContext context, SnPollQuestion q) {
if (widget.stats == null) return const SizedBox.shrink(); return PollStatsWidget(question: q, stats: widget.stats);
final raw = widget.stats![q.id];
if (raw == null) return const SizedBox.shrink();
Widget? body;
switch (q.type) {
case SnPollQuestionType.rating:
// rating: avg score (double or int)
final avg = (raw['rating'] as num?)?.toDouble();
if (avg == null) break;
final theme = Theme.of(context);
body = Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(Icons.star, color: Colors.amber.shade600, size: 18),
const SizedBox(width: 6),
Text(
avg.toStringAsFixed(1),
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
break;
case SnPollQuestionType.yesNo:
// yes/no: map {true: count, false: count}
if (raw is Map) {
final int yes =
(raw[true] is int)
? raw[true] as int
: int.tryParse('${raw[true]}') ?? 0;
final int no =
(raw[false] is int)
? raw[false] as int
: int.tryParse('${raw[false]}') ?? 0;
final total = (yes + no).clamp(0, 1 << 31);
final yesPct = total == 0 ? 0.0 : yes / total;
final noPct = total == 0 ? 0.0 : no / total;
final theme = Theme.of(context);
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_BarStatRow(
label: 'Yes',
count: yes,
fraction: yesPct,
color: Colors.green.shade600,
),
const SizedBox(height: 6),
_BarStatRow(
label: 'No',
count: no,
fraction: noPct,
color: Colors.red.shade600,
),
const SizedBox(height: 4),
Text(
'Total: $total',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.singleChoice:
case SnPollQuestionType.multipleChoice:
// map optionId -> count
if (raw is Map) {
final options = [...?q.options]
..sort((a, b) => a.order.compareTo(b.order));
final List<_OptionCount> items = [];
int total = 0;
for (final opt in options) {
final dynamic v = raw[opt.id];
final int count = v is int ? v : int.tryParse('$v') ?? 0;
total += count;
items.add(_OptionCount(id: opt.id, label: opt.label, count: count));
}
if (items.isNotEmpty) {
items.sort(
(a, b) => b.count.compareTo(a.count),
); // show highest first
}
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final it in items)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: _BarStatRow(
label: it.label,
count: it.count,
fraction: total == 0 ? 0 : it.count / total,
),
),
if (items.isNotEmpty)
Text(
'Total: $total',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.freeText:
// No stats
break;
}
if (body == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 8),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Stats',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
body,
],
),
),
),
);
} }
Widget _buildBody(BuildContext context) { Widget _buildBody(BuildContext context) {
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying
}
final q = _current; final q = _current;
switch (q.type) { switch (q.type) {
case SnPollQuestionType.singleChoice: case SnPollQuestionType.singleChoice:
@@ -517,9 +394,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
children: [ children: [
Expanded( Expanded(
child: SegmentedButton<bool>( child: SegmentedButton<bool>(
segments: const [ segments: [
ButtonSegment(value: true, label: Text('Yes')), ButtonSegment(value: true, label: Text('yes'.tr())),
ButtonSegment(value: false, label: Text('No')), ButtonSegment(value: false, label: Text('no'.tr())),
], ],
selected: _yesNoSelected == null ? {} : {_yesNoSelected!}, selected: _yesNoSelected == null ? {} : {_yesNoSelected!},
onSelectionChanged: (sel) { onSelectionChanged: (sel) {
@@ -568,12 +445,39 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
final isLast = _index == _questions.length - 1; final isLast = _index == _questions.length - 1;
final canProceed = _isCurrentAnswered() && !_submitting; final canProceed = _isCurrentAnswered() && !_submitting;
if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) {
// If poll is submitted and not in modification mode, show "Modify" button
return FilledButton.icon(
icon: const Icon(Icons.edit),
label: Text('modifyAnswers'.tr()),
onPressed: () {
setState(() {
_isModifying = true;
_index = 0; // Reset to first question for modification
_loadCurrentIntoLocalState();
});
},
);
}
return Row( return Row(
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
label: Text(_index == 0 ? 'Cancel' : 'Back'), label: Text(_index == 0 ? 'cancel'.tr() : 'back'.tr()),
onPressed: _submitting ? null : _back, onPressed:
_submitting
? null
: () {
if (_index == 0 && _isModifying) {
// If at first question and in modification mode, go back to submitted view
setState(() {
_isModifying = false;
});
} else {
_back();
}
},
), ),
const Spacer(), const Spacer(),
FilledButton.icon( FilledButton.icon(
@@ -585,19 +489,188 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: Icon(isLast ? Icons.check : Icons.arrow_forward), : Icon(isLast ? Icons.check : Icons.arrow_forward),
label: Text(isLast ? 'Submit' : 'Next'), label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
onPressed: canProceed ? _next : null, onPressed: canProceed ? _next : null,
), ),
], ],
); );
} }
Widget _buildSubmittedView(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.poll.title != null || widget.poll.description != null)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.poll.title?.isNotEmpty ?? false)
Text(
widget.poll.title!,
style: Theme.of(context).textTheme.titleLarge,
),
if (widget.poll.description?.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
widget.poll.description!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(
context,
).textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
),
],
),
),
for (final q in _questions)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
q.title,
style: Theme.of(context).textTheme.titleMedium,
),
),
if (q.isRequired)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'*',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
if (q.description != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
q.description!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
),
),
_buildStats(context, q),
],
),
),
],
);
}
Widget _buildReadonlyView(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.poll.title != null || widget.poll.description != null)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.poll.title != null)
Text(
widget.poll.title!,
style: Theme.of(context).textTheme.titleLarge,
),
if (widget.poll.description != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
widget.poll.description!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(
context,
).textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
),
],
),
),
for (final q in _questions)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
q.title,
style: Theme.of(context).textTheme.titleMedium,
),
),
if (q.isRequired)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'*',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
if (q.description != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
q.description!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
),
),
_buildStats(context, q),
],
),
),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_questions.isEmpty) { if (_questions.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
// If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [_buildSubmittedView(context), _buildNavBar(context)],
);
}
// If poll is in readonly mode, show readonly view
if (widget.isReadonly) {
return _buildReadonlyView(context);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@@ -617,77 +690,6 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
} }
} }
class _OptionCount {
final String id;
final String label;
final int count;
const _OptionCount({
required this.id,
required this.label,
required this.count,
});
}
class _BarStatRow extends StatelessWidget {
const _BarStatRow({
required this.label,
required this.count,
required this.fraction,
this.color,
});
final String label;
final int count;
final double fraction;
final Color? color;
@override
Widget build(BuildContext context) {
final barColor = color ?? Theme.of(context).colorScheme.primary;
final bgColor = Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.6);
final fg =
(fraction.isNaN || fraction.isInfinite)
? 0.0
: fraction.clamp(0.0, 1.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label · $count', style: Theme.of(context).textTheme.labelMedium),
const SizedBox(height: 4),
LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final filled = width * fg;
return Stack(
children: [
Container(
height: 8,
width: width,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(999),
),
),
Container(
height: 8,
width: filled,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(999),
),
),
],
);
},
),
],
);
}
}
/// Simple fade/slide transition between questions. /// Simple fade/slide transition between questions.
class _AnimatedStep extends StatelessWidget { class _AnimatedStep extends StatelessWidget {
const _AnimatedStep({super.key, required this.child}); const _AnimatedStep({super.key, required this.child});

View File

@@ -186,10 +186,9 @@ class ComposePollSheet extends HookConsumerWidget {
); );
} }
Widget? _buildPollSubtitle(SnPoll poll) { Widget? _buildPollSubtitle(SnPollWithStats poll) {
try { try {
final SnPoll dyn = poll; final List<SnPollQuestion> options = poll.questions;
final List<SnPollQuestion> options = dyn.questions;
if (options.isEmpty) return null; if (options.isEmpty) return null;
final preview = options.take(3).map((e) => e.title).join(' · '); final preview = options.take(3).map((e) => e.title).join(' · ');
if (preview.trim().isEmpty) return null; if (preview.trim().isEmpty) return null;

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,9 @@ import 'package:island/models/post.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart'; import 'package:super_context_menu/super_context_menu.dart';
class PostItemCreator extends HookConsumerWidget { class PostItemCreator extends HookConsumerWidget {
@@ -81,7 +79,6 @@ class PostItemCreator extends HookConsumerWidget {
title: 'copyLink'.tr(), title: 'copyLink'.tr(),
image: MenuImage.icon(Symbols.link), image: MenuImage.icon(Symbols.link),
callback: () { callback: () {
// Copy post link to clipboard
context.pushNamed( context.pushNamed(
'postDetail', 'postDetail',
pathParameters: {'id': item.id}, pathParameters: {'id': item.id},
@@ -105,8 +102,9 @@ class PostItemCreator extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildPostHeader(context), PostHeader(item: item),
_buildPostContent(context), PostBody(item: item),
ReferencedPostWidget(item: item),
const Gap(16), const Gap(16),
_buildAnalyticsSection(context), _buildAnalyticsSection(context),
], ],
@@ -117,128 +115,12 @@ class PostItemCreator extends HookConsumerWidget {
); );
} }
Widget _buildPostHeader(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Post ID and timestamp row
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'ID: ${item.id.substring(0, 6)}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
const Spacer(),
Icon(
_getVisibilityIcon(item.visibility),
size: 16,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
_getVisibilityText(item.visibility).tr(),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.secondary,
),
),
const Gap(8),
Text(
item.publishedAt?.formatSystem() ?? '',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
const Gap(8),
// Title and description
if (item.title?.isNotEmpty ?? false)
Text(
item.title!,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
if (item.description?.isNotEmpty ?? false)
Text(
item.description!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
).padding(top: 4),
],
);
}
Widget _buildPostContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Content preview
if (item.content?.isNotEmpty ?? false)
Container(
margin: const EdgeInsets.only(top: 12),
child: MarkdownTextContent(content: item.content!),
),
// Attachments
if (item.attachments.isNotEmpty)
CloudFileList(
files: item.attachments,
maxWidth: MediaQuery.of(context).size.width * 0.85,
padding: EdgeInsets.only(top: 8),
),
// Reference post indicator
if (item.repliedPost != null || item.forwardedPost != null)
Container(
margin: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
item.repliedPost != null ? Symbols.reply : Symbols.forward,
size: 16,
color: Theme.of(context).colorScheme.secondary,
),
const Gap(4),
Text(
item.repliedPost != null
? 'repliedTo'.tr()
: 'forwarded'.tr(),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
),
],
);
}
Widget _buildAnalyticsSection(BuildContext context) { Widget _buildAnalyticsSection(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Analytics', style: Theme.of(context).textTheme.titleSmall), Text('Analytics', style: Theme.of(context).textTheme.titleSmall),
const Gap(8), const Gap(8),
// Engagement metrics in a card
Card( Card(
elevation: 1, elevation: 1,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
@@ -279,15 +161,9 @@ class PostItemCreator extends HookConsumerWidget {
), ),
), ),
const Gap(16), const Gap(16),
// Reactions summary
if (item.reactionsCount.isNotEmpty) _buildReactionsSection(context), if (item.reactionsCount.isNotEmpty) _buildReactionsSection(context),
// Metadata section
if (item.meta != null && item.meta!.isNotEmpty) if (item.meta != null && item.meta!.isNotEmpty)
_buildMetadataSection(context), _buildMetadataSection(context),
// Creation and modification timestamps
const Gap(16), const Gap(16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -425,31 +301,3 @@ class PostItemCreator extends HookConsumerWidget {
); );
} }
} }
// Helper method to get the appropriate icon for each visibility status
IconData _getVisibilityIcon(int visibility) {
switch (visibility) {
case 1: // Friends
return Symbols.group;
case 2: // Unlisted
return Symbols.link_off;
case 3: // Private
return Symbols.lock;
default: // Public (0) or unknown
return Symbols.public;
}
}
// Helper method to get the translation key for each visibility status
String _getVisibilityText(int visibility) {
switch (visibility) {
case 1: // Friends
return 'postVisibilityFriends';
case 2: // Unlisted
return 'postVisibilityUnlisted';
case 3: // Private
return 'postVisibilityPrivate';
default: // Public (0) or unknown
return 'postVisibilityPublic';
}
}

View File

@@ -0,0 +1,135 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:styled_widget/styled_widget.dart';
class PostItemScreenshot extends ConsumerWidget {
final SnPost item;
final EdgeInsets? padding;
final bool isFullPost;
final bool isShowReference;
const PostItemScreenshot({
super.key,
required this.item,
this.padding,
this.isFullPost = false,
this.isShowReference = true,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final renderingPadding =
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
final mostReaction =
item.reactionsCount.isEmpty
? null
: item.reactionsCount.entries
.sortedBy((e) => e.value)
.map((e) => e.key)
.last;
final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;
return Material(
elevation: 0,
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gap(renderingPadding.vertical),
PostHeader(
item: item,
isFullPost: isFullPost,
isInteractive: false,
renderingPadding: renderingPadding,
isRelativeTime: false,
trailing:
mostReaction != null
? Row(
children: [
Text(
kReactionTemplates[mostReaction]?.icon ?? '',
style: const TextStyle(fontSize: 20),
),
const Gap(4),
Text(
'x${item.reactionsCount[mostReaction]}',
style: const TextStyle(fontSize: 11),
),
],
)
: null,
),
PostBody(
item: item,
renderingPadding: renderingPadding,
isFullPost: isFullPost,
isTextSelectable: false,
isInteractive: false,
),
if (isShowReference)
ReferencedPostWidget(
item: item,
isInteractive: false,
renderingPadding: renderingPadding,
),
Container(
color: Theme.of(context).colorScheme.surfaceContainerLow,
margin: const EdgeInsets.only(top: 8),
padding: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 4,
),
child: Row(
children: [
SizedBox(
width: 44,
height: 44,
child: Image.asset(
'assets/icons/icon${isDark ? '-dark' : ''}.png',
width: 40,
height: 40,
),
).padding(vertical: 8, right: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Solar Network',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const Text(
'sharePostSlogan',
style: TextStyle(fontSize: 12),
).tr().opacity(0.9),
],
),
),
QrImageView(
data: 'https://solian.app/posts/${item.id}',
version: QrVersions.auto,
size: 60,
errorCorrectionLevel: QrErrorCorrectLevel.M,
backgroundColor: Colors.transparent,
foregroundColor: Theme.of(context).colorScheme.onSurface,
padding: const EdgeInsets.all(8),
),
],
),
),
],
),
);
}
}

View File

@@ -1,11 +1,13 @@
import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart'; import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/post/publishers_modal.dart';
@@ -14,8 +16,14 @@ import 'package:styled_widget/styled_widget.dart';
class PostQuickReply extends HookConsumerWidget { class PostQuickReply extends HookConsumerWidget {
final SnPost parent; final SnPost parent;
final Function? onPosted; final VoidCallback? onPosted;
const PostQuickReply({super.key, required this.parent, this.onPosted}); final VoidCallback? onLaunch;
const PostQuickReply({
super.key,
required this.parent,
this.onPosted,
this.onLaunch,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -48,7 +56,7 @@ class PostQuickReply extends HookConsumerWidget {
'content': contentController.text, 'content': contentController.text,
'replied_post_id': parent.id, 'replied_post_id': parent.id,
}, },
options: Options(headers: {'X-Pub': currentPublisher.value?.name}), queryParameters: {'pub': currentPublisher.value?.name},
); );
contentController.clear(); contentController.clear();
onPosted?.call(); onPosted?.call();
@@ -83,9 +91,10 @@ class PostQuickReply extends HookConsumerWidget {
child: TextField( child: TextField(
controller: contentController, controller: contentController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Post your reply', hintText: 'postReplyPlaceholder'.tr(),
border: const OutlineInputBorder(), border: InputBorder.none,
isDense: true, isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
@@ -97,6 +106,26 @@ class PostQuickReply extends HookConsumerWidget {
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
IconButton(
onPressed: () {
onLaunch?.call();
GoRouter.of(context)
.pushNamed(
'postCompose',
extra: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
)
.then((value) {
if (value != null) onPosted?.call();
});
},
icon: const Icon(Symbols.launch, size: 20),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
),
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -110,6 +139,7 @@ class PostQuickReply extends HookConsumerWidget {
: Icon(Symbols.send, size: 20), : Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction, onPressed: submitting.value ? null : performAction,
constraints: const BoxConstraints(),
), ),
], ],
), ),

View File

@@ -38,14 +38,18 @@ class PostRepliesSheet extends HookConsumerWidget {
if (user.value != null) if (user.value != null)
Material( Material(
elevation: 2, elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: PostQuickReply( child: PostQuickReply(
parent: post, parent: post,
onPosted: () { onPosted: () {
ref.invalidate(postRepliesNotifierProvider(post.id)); ref.invalidate(postRepliesNotifierProvider(post.id));
}, },
onLaunch: () {
Navigator.of(context).pop();
},
).padding( ).padding(
bottom: MediaQuery.of(context).padding.bottom + 16, bottom: MediaQuery.of(context).padding.bottom + 8,
top: 16, top: 8,
horizontal: 16, horizontal: 16,
), ),
), ),

View File

@@ -0,0 +1,841 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/embed.dart';
import 'package:island/models/poll.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/poll/poll_submit.dart';
import 'package:island/widgets/post/post_replies_sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'post_shared.g.dart';
@riverpod
Future<SnPost?> postFeaturedReply(Ref ref, String id) async {
final client = ref.watch(apiClientProvider);
try {
final resp = await client.get('/sphere/posts/$id/replies/featured');
return SnPost.fromJson(resp.data);
} catch (_) {
return null;
}
}
class PostVisibilityHelpers {
static IconData getVisibilityIcon(int visibility) {
switch (visibility) {
case 1:
return Symbols.group;
case 2:
return Symbols.link_off;
case 3:
return Symbols.lock;
default:
return Symbols.public;
}
}
static String getVisibilityText(int visibility) {
switch (visibility) {
case 1:
return 'postVisibilityFriends';
case 2:
return 'postVisibilityUnlisted';
case 3:
return 'postVisibilityPrivate';
default:
return 'postVisibilityPublic';
}
}
}
class PostReplyPreview extends HookConsumerWidget {
final SnPost parent;
final bool isOpenable;
final bool isCompact;
final bool isAutoload;
final VoidCallback? onOpen;
const PostReplyPreview({
super.key,
required this.parent,
this.isOpenable = false,
this.isCompact = false,
this.isAutoload = true,
this.onOpen,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final posts = useState<List<SnPost>>([]);
final loading = useState(false);
Future<void> fetchMoreReplies({int pageSize = 3}) async {
final client = ref.read(apiClientProvider);
loading.value = true;
try {
final response = await client.get(
'/sphere/posts/${parent.id}/replies',
queryParameters: {'offset': posts.value.length, 'take': pageSize},
);
try {
posts.value = [
...posts.value,
...response.data.map((e) => SnPost.fromJson(e)),
];
} catch (_) {
// ignore disposed
}
} catch (err) {
showErrorAlert(err);
} finally {
try {
loading.value = false;
} catch (_) {
// ignore disposed
}
}
}
useEffect(() {
if (isAutoload) fetchMoreReplies();
return null;
}, [parent]);
final featuredReply =
isOpenable ? null : ref.watch(PostFeaturedReplyProvider(parent.id));
final itemWidget =
isOpenable
? Column(
children: [
for (final post in posts.value)
Column(
children: [
InkWell(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ProfilePictureWidget(
file: post.publisher.picture,
radius: 12,
).padding(top: 4),
if (post.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(
content: post.content!,
).padding(top: 2),
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(post.attachments.length),
),
],
),
onTap: () {
onOpen?.call();
context.pushNamed(
'postDetail',
pathParameters: {'id': post.id},
);
},
),
if (post.repliesCount > 0)
PostReplyPreview(
parent: post,
isOpenable: true,
isCompact: true,
isAutoload: false,
onOpen: onOpen,
).padding(left: 24),
],
),
if (loading.value)
Row(
spacing: 8,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(),
),
Text('loading').tr(),
],
)
else if (posts.value.length < parent.repliesCount)
InkWell(
child: Row(
spacing: 8,
children: [
const Icon(Symbols.keyboard_arrow_down, size: 20),
Text('repliesLoadMore').tr(),
],
),
onTap: () {
fetchMoreReplies();
},
),
],
)
: (featuredReply!).map(
data:
(data) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [
ProfilePictureWidget(
file: data.value?.publisher.picture,
radius: 12,
).padding(top: 4),
if (data.value?.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(
content: data.value!.content!,
),
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(data.value?.attachments.length ?? 0),
),
],
),
error:
(e) => Row(
spacing: 8,
children: [
const Icon(Symbols.close, size: 18),
Text(e.error.toString()),
],
),
loading:
(_) => Row(
spacing: 8,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(),
),
Text('loading').tr(),
],
),
);
final contentWidget =
isCompact
? itemWidget
: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 4,
children: [
Text('repliesCount')
.plural(parent.repliesCount)
.fontSize(15)
.bold()
.padding(horizontal: 5),
itemWidget,
],
),
);
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => PostRepliesSheet(post: parent),
);
},
child: contentWidget,
);
}
}
class PostTruncateHint extends StatelessWidget {
final bool isCompact;
final EdgeInsets? margin;
final bool withArrow;
const PostTruncateHint({
super.key,
this.isCompact = false,
this.margin,
this.withArrow = false,
});
@override
Widget build(BuildContext context) {
return Container(
margin: margin ?? EdgeInsets.only(top: isCompact ? 4 : 8),
padding: EdgeInsets.symmetric(
horizontal: isCompact ? 8 : 12,
vertical: isCompact ? 4 : 8,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.more_horiz,
size: isCompact ? 14 : 16,
color: Theme.of(context).colorScheme.secondary,
),
SizedBox(width: isCompact ? 4 : 6),
Flexible(
child: Text(
'postTruncated'.tr(),
style: TextStyle(
fontSize: isCompact ? 10 : 12,
color: Theme.of(context).colorScheme.secondary,
fontStyle: FontStyle.italic,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (withArrow) ...[
SizedBox(width: isCompact ? 3 : 4),
Icon(
Symbols.arrow_forward,
size: isCompact ? 12 : 14,
color: Theme.of(context).colorScheme.secondary,
),
],
],
),
);
}
}
class ReferencedPostWidget extends StatelessWidget {
final SnPost item;
final bool isInteractive;
final EdgeInsets renderingPadding;
const ReferencedPostWidget({
super.key,
required this.item,
this.isInteractive = true,
this.renderingPadding = EdgeInsets.zero,
});
@override
Widget build(BuildContext context) {
final referencePost = item.repliedPost ?? item.forwardedPost;
if (referencePost == null) return const SizedBox.shrink();
final isReply = item.repliedPost != null;
final content = Container(
padding: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 8,
),
margin: EdgeInsets.only(
top: 8,
left: renderingPadding.vertical,
right: renderingPadding.vertical,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isReply ? Symbols.reply : Symbols.forward,
size: 16,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 6),
Text(
isReply ? 'repliedTo'.tr() : 'forwarded'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
],
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: referencePost.publisher.picture?.id,
radius: 16,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
referencePost.publisher.nick,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
if (referencePost.visibility != 0)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
PostVisibilityHelpers.getVisibilityIcon(
referencePost.visibility,
),
size: 12,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
PostVisibilityHelpers.getVisibilityText(
referencePost.visibility,
).tr(),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.secondary,
),
),
],
).padding(top: 2, bottom: 2),
if (referencePost.title?.isNotEmpty ?? false)
Text(
referencePost.title!,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: Theme.of(context).colorScheme.onSurface,
),
).padding(top: 2, bottom: 2),
if (referencePost.description?.isNotEmpty ?? false)
Text(
referencePost.description!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
).padding(bottom: 2),
if (referencePost.content?.isNotEmpty ?? false)
MarkdownTextContent(
content: referencePost.content!,
textStyle: const TextStyle(fontSize: 14),
isSelectable: false,
linesMargin:
referencePost.type == 0
? const EdgeInsets.only(bottom: 4)
: null,
attachments: item.attachments,
).padding(bottom: 4),
if (referencePost.isTruncated)
const PostTruncateHint(
isCompact: true,
margin: EdgeInsets.only(top: 4, bottom: 8),
),
if (referencePost.attachments.isNotEmpty &&
referencePost.type != 1)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.attach_file,
size: 12,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
'postHasAttachments'.plural(
referencePost.attachments.length,
),
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontSize: 12,
),
),
],
).padding(vertical: 2),
],
),
),
],
),
],
),
);
if (!isInteractive) {
return content;
}
return content.gestures(
onTap:
() => context.pushNamed(
'postDetail',
pathParameters: {'id': referencePost.id},
),
);
}
}
class PostHeader extends StatelessWidget {
final SnPost item;
final bool isFullPost;
final Widget? trailing;
final bool isInteractive;
final EdgeInsets renderingPadding;
final bool isRelativeTime;
const PostHeader({
super.key,
required this.item,
this.isFullPost = false,
this.trailing,
this.isInteractive = true,
this.renderingPadding = EdgeInsets.zero,
this.isRelativeTime = true,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 12,
children: [
GestureDetector(
onTap:
isInteractive
? () {
context.pushNamed(
'publisherProfile',
pathParameters: {'name': item.publisher.name},
);
}
: null,
child: ProfilePictureWidget(file: item.publisher.picture, radius: 16),
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 4,
children: [
Text(item.publisher.nick).bold(),
if (item.publisher.verification != null)
VerificationMark(mark: item.publisher.verification!),
Text('@${item.publisher.name}').fontSize(11),
],
),
Row(
spacing: 6,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
!isFullPost && isRelativeTime
? (item.publishedAt ?? item.createdAt)!.formatRelative(
context,
)
: (item.publishedAt ?? item.createdAt)!.formatSystem(),
).fontSize(10),
if (item.editedAt != null)
Text(
'editedAt'.tr(
args: [
!isFullPost && isRelativeTime
? item.editedAt!.formatRelative(context)
: item.editedAt!.formatSystem(),
],
),
).fontSize(10),
if (item.visibility != 0)
Text(
PostVisibilityHelpers.getVisibilityText(
item.visibility,
).tr(),
).fontSize(10),
],
),
],
),
),
if (trailing != null) trailing!,
],
).padding(horizontal: renderingPadding.horizontal, bottom: 4);
}
}
class PostBody extends ConsumerWidget {
final SnPost item;
final bool isFullPost;
final bool isTextSelectable;
final Widget? translationSection;
final bool isInteractive;
final EdgeInsets renderingPadding;
const PostBody({
super.key,
required this.item,
this.isFullPost = false,
this.isTextSelectable = true,
this.translationSection,
this.isInteractive = true,
this.renderingPadding = EdgeInsets.zero,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isFullPost && item.type == 1)
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: EdgeInsets.only(
top: 4,
left: renderingPadding.horizontal,
right: renderingPadding.vertical,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.centerLeft,
child: Badge(
label: const Text('postArticle').tr(),
backgroundColor: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
const Gap(4),
if (item.title != null)
Text(
item.title!,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.bold,
),
),
if (item.description != null)
Text(
item.description!,
style: Theme.of(context).textTheme.bodyMedium,
)
else
MarkdownTextContent(content: '${item.content!}...'),
],
),
)
else if ((item.content?.isNotEmpty ?? false) ||
(item.title?.isNotEmpty ?? false) ||
(item.description?.isNotEmpty ?? false))
Padding(
padding: EdgeInsets.only(
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if ((item.title?.isNotEmpty ?? false) ||
(item.description?.isNotEmpty ?? false))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.title?.isNotEmpty ?? false)
Text(
item.title!,
style: Theme.of(context).textTheme.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
if (item.description?.isNotEmpty ?? false)
Text(
item.description!,
style: Theme.of(context).textTheme.bodyMedium,
),
],
).padding(bottom: 4),
MarkdownTextContent(
content:
item.isTruncated ? '${item.content!}...' : item.content!,
isSelectable: isTextSelectable,
),
if (translationSection != null) translationSection!,
],
),
),
if (item.isTruncated && item.type != 1)
PostTruncateHint(
isCompact: true,
withArrow: isInteractive,
margin: EdgeInsets.only(
top: 4,
bottom: 4,
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
),
),
if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList(
files: item.attachments,
isColumn: !isInteractive,
padding: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 4,
),
),
if (item.tags.isNotEmpty || item.categories.isNotEmpty)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: [
if (item.tags.isNotEmpty)
Wrap(
runAlignment: WrapAlignment.center,
spacing: 8,
children: [
const Icon(Symbols.label, size: 16).padding(top: 2),
for (final tag
in isFullPost ? item.tags : item.tags.take(3))
InkWell(
onTap:
isInteractive
? () {
GoRouter.of(context).pushNamed(
'postTagDetail',
pathParameters: {'slug': tag.slug},
);
}
: null,
child: Text('#${tag.name ?? tag.slug}'),
),
if (!isFullPost && item.tags.length > 3)
Text('+${item.tags.length - 3}').opacity(0.6),
],
),
if (item.categories.isNotEmpty)
Wrap(
runAlignment: WrapAlignment.center,
spacing: 8,
children: [
const Icon(Symbols.category, size: 16).padding(top: 2),
for (final category
in isFullPost
? item.categories
: item.categories.take(2))
InkWell(
onTap:
isInteractive
? () {
GoRouter.of(context).pushNamed(
'postCategoryDetail',
pathParameters: {'slug': category.slug},
);
}
: null,
child: Text(category.categoryDisplayTitle),
),
if (!isFullPost && item.categories.length > 2)
Text('+${item.categories.length - 2}').opacity(0.6),
],
),
],
).padding(horizontal: renderingPadding.horizontal + 4, top: 4),
if (item.meta?['embeds'] != null)
...((item.meta!['embeds'] as List<dynamic>)
.map((embedData) => convertMapKeysToSnakeCase(embedData))
.map(
(embedData) => switch (embedData['type']) {
'link' => EmbedLinkWidget(
link: SnScrappedLink.fromJson(embedData),
maxWidth: math.min(
MediaQuery.of(context).size.width,
kWideScreenWidth,
),
margin: EdgeInsets.only(
top: 4,
bottom: 4,
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
),
),
'poll' => Card(
margin: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 8,
),
child:
embedData['poll'] == null
? const Text('Poll was not loaded...')
: PollSubmit(
initialAnswers:
embedData['poll']?['user_answer']?['answer'],
stats: embedData['poll']?['stats'],
poll: SnPollWithStats.fromJson(embedData['poll']),
onSubmit: (_) {},
isReadonly: !isInteractive,
).padding(horizontal: 16, vertical: 12),
),
_ => Text('Unable show embed: ${embedData['type']}'),
},
)),
],
);
}
}

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_item.dart'; part of 'post_shared.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator

View File

@@ -10,7 +10,9 @@ import connectivity_plus
import device_info_plus import device_info_plus
import file_picker import file_picker
import file_selector_macos import file_selector_macos
import firebase_analytics
import firebase_core import firebase_core
import firebase_crashlytics
import firebase_messaging import firebase_messaging
import flutter_inappwebview_macos import flutter_inappwebview_macos
import flutter_platform_alert import flutter_platform_alert
@@ -44,7 +46,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin")) FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))

View File

@@ -13,23 +13,64 @@ PODS:
- FlutterMacOS - FlutterMacOS
- Firebase/CoreOnly (12.0.0): - Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0) - FirebaseCore (~> 12.0.0)
- Firebase/Crashlytics (12.0.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.0.0)
- Firebase/Messaging (12.0.0): - Firebase/Messaging (12.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.0.0) - FirebaseMessaging (~> 12.0.0)
- firebase_analytics (12.0.0):
- firebase_core
- FirebaseAnalytics (= 12.0.0)
- FlutterMacOS
- firebase_core (4.0.0): - firebase_core (4.0.0):
- Firebase/CoreOnly (~> 12.0.0) - Firebase/CoreOnly (~> 12.0.0)
- FlutterMacOS - FlutterMacOS
- firebase_crashlytics (5.0.0):
- Firebase/CoreOnly (~> 12.0.0)
- Firebase/Crashlytics (~> 12.0.0)
- firebase_core
- FlutterMacOS
- firebase_messaging (16.0.0): - firebase_messaging (16.0.0):
- Firebase/CoreOnly (~> 12.0.0) - Firebase/CoreOnly (~> 12.0.0)
- Firebase/Messaging (~> 12.0.0) - Firebase/Messaging (~> 12.0.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- FirebaseAnalytics (12.0.0):
- FirebaseAnalytics/Default (= 12.0.0)
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleAppMeasurement/Default (= 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.0.0): - FirebaseCore (12.0.0):
- FirebaseCoreInternal (~> 12.0.0) - FirebaseCoreInternal (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreInternal (12.0.0): - FirebaseCoreInternal (12.0.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseRemoteConfigInterop (~> 12.0.0)
- FirebaseSessions (~> 12.0.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (12.0.0): - FirebaseInstallations (12.0.0):
- FirebaseCore (~> 12.0.0) - FirebaseCore (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
@@ -44,6 +85,16 @@ PODS:
- GoogleUtilities/Reachability (~> 8.1) - GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (12.0.0)
- FirebaseSessions (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreExtension (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- flutter_inappwebview_macos (0.0.1): - flutter_inappwebview_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- OrderedSet (~> 6.0.3) - OrderedSet (~> 6.0.3)
@@ -63,6 +114,28 @@ PODS:
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAppMeasurement/Core (12.0.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.0.0):
- GoogleAdsOnDeviceConversion (= 2.1.0)
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleAppMeasurement/IdentitySupport (= 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.0.0):
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0): - GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
@@ -76,6 +149,9 @@ PODS:
- GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment - GoogleUtilities/Environment
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0): - GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib" - "GoogleUtilities/NSData+zlib"
@@ -117,6 +193,8 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- record_macos (1.0.0): - record_macos (1.0.0):
- FlutterMacOS - FlutterMacOS
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
@@ -130,25 +208,25 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.50.3): - sqlite3 (3.50.4):
- sqlite3/common (= 3.50.3) - sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.3) - sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.3): - sqlite3/dbstatvtab (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.50.3): - sqlite3/fts5 (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.50.3): - sqlite3/math (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.50.3): - sqlite3/perf-threadsafe (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.50.3): - sqlite3/rtree (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/session (3.50.3): - sqlite3/session (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.50.3) - sqlite3 (~> 3.50.4)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/math - sqlite3/math
@@ -172,7 +250,9 @@ DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/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`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
- firebase_crashlytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos`)
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
- flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`) - flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
@@ -204,15 +284,22 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- Firebase - Firebase
- FirebaseAnalytics
- FirebaseCore - FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal - FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations - FirebaseInstallations
- FirebaseMessaging - FirebaseMessaging
- FirebaseRemoteConfigInterop
- FirebaseSessions
- GoogleAppMeasurement
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
- PromisesSwift
- SAMKeychain - SAMKeychain
- sqlite3 - sqlite3
- WebRTC-SDK - WebRTC-SDK
@@ -230,8 +317,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_selector_macos: file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
firebase_analytics:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos
firebase_core: firebase_core:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
firebase_crashlytics:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos
firebase_messaging: firebase_messaging:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
flutter_inappwebview_macos: flutter_inappwebview_macos:
@@ -295,12 +386,19 @@ SPEC CHECKSUMS:
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: 53f0dc87ad10f56a6df8746da60d8a5fe41f886f
firebase_core: eeea10f64026b68cd0bc3dee079ab4717e22909e firebase_core: eeea10f64026b68cd0bc3dee079ab4717e22909e
firebase_crashlytics: 7be1dacc38809971354def57193b280636a3d51a
firebase_messaging: 5eefcd5bde556bfacdd9968e11c52f39032dfbe5 firebase_messaging: 5eefcd5bde556bfacdd9968e11c52f39032dfbe5
FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7
FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a
FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361
FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55
FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613
FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
@@ -309,6 +407,7 @@ SPEC CHECKSUMS:
flutter_webrtc: 0d70bd8782c19bde286dc52f766eebbea26de201 flutter_webrtc: 0d70bd8782c19bde286dc52f766eebbea26de201
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: baecd024ebfd13c441269ca7404792a7152fde89 gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
@@ -322,14 +421,15 @@ SPEC CHECKSUMS:
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0 record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd

View File

@@ -234,6 +234,7 @@
3399D490228B24CF009A79C7 /* ShellScript */, 3399D490228B24CF009A79C7 /* ShellScript */,
F1E275A871246799FC3019F6 /* [CP] Embed Pods Frameworks */, F1E275A871246799FC3019F6 /* [CP] Embed Pods Frameworks */,
8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */, 8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */,
6B512DBE9D8E74A09686E70F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
); );
buildRules = ( buildRules = (
); );
@@ -376,6 +377,24 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
6B512DBE9D8E74A09686E70F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=macos --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n";
};
8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */ = { 8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;

View File

@@ -73,22 +73,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
auto_route:
dependency: transitive
description:
name: auto_route
sha256: b8c036fa613a98a759cf0fdcba26e62f4985dcbff01a5e760ab411e8554bbaf0
url: "https://pub.dev"
source: hosted
version: "10.1.0+1"
auto_route_generator:
dependency: "direct dev"
description:
name: auto_route_generator
sha256: "9e3846fcbeacba5c362557328dd8c8fbc953b6a0cbc3395365e8d8f92eea29c4"
url: "https://pub.dev"
source: hosted
version: "10.1.0"
avatar_stack: avatar_stack:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -205,10 +189,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62" sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.11.0" version: "8.11.1"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -317,10 +301,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: connectivity_plus name: connectivity_plus
sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "6.1.5"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -329,6 +313,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
console:
dependency: transitive
description:
name: console
sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a
url: "https://pub.dev"
source: hosted
version: "4.1.0"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@@ -421,10 +413,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_webrtc name: dart_webrtc
sha256: a2ae542cdadc21359022adedc26138fa3487cc3b3547c24ff4f556681869e28c sha256: "3bfa069a8b14a53ba506f6dd529e9b88c878ba0cc238f311051a39bf1e53d075"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.3+hotfix.4" version: "1.5.3+hotfix.5"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@@ -453,10 +445,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.8.0+1" version: "5.9.0"
dio_web_adapter: dio_web_adapter:
dependency: transitive dependency: transitive
description: description:
@@ -573,10 +565,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "13ba4e627ef24503a465d1d61b32596ce10eb6b8903678d362a528f9939b4aa8" sha256: ef7d2a085c1b1d69d17b6842d0734aad90156de08df6bd3c12496d0bd6ddf8e2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.2.1" version: "10.3.1"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@@ -609,6 +601,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+4" version: "0.9.3+4"
firebase_analytics:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "07146e89e11302c6b07e3465c2c556ebcdd0053a3c5b1aa9bfd3203b778e5b4c"
url: "https://pub.dev"
source: hosted
version: "12.0.0"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: "27e81a0efc821bec6cba64abc1083b91c8ddbad28eeb4c6f6b7c78a59d06f259"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: "7d87f47462042a7d9125e3123db2783bc72917d85e2719d4cb6aeaec209605e1"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -633,6 +649,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "95b6871850b1a7e3b09c284c59a0c71fafcad3eee8ac1b6f06aaf8979290cbb8"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: ba5b7a916f1ebedc6db35b33abdc618f202fc25e0792088dfba698e19fec9c09
url: "https://pub.dev"
source: hosted
version: "3.8.11"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -678,6 +710,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_app_update:
dependency: "direct main"
description:
name: flutter_app_update
sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
flutter_blurhash: flutter_blurhash:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -714,10 +754,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_hooks name: flutter_hooks
sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d sha256: c3df76c62bb3a9f9bee75c57cdab40abab6123b734c1cd7e9b26a5dbd436eceb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.21.2" version: "0.21.3"
flutter_inappwebview: flutter_inappwebview:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -911,10 +951,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.28" version: "2.0.29"
flutter_popup_card: flutter_popup_card:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1033,10 +1073,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: font_awesome_flutter name: font_awesome_flutter
sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.8.0" version: "10.9.0"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -1077,6 +1117,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
get_it:
dependency: transitive
description:
name: get_it
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
url: "https://pub.dev"
source: hosted
version: "8.2.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@@ -1089,10 +1137,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431 sha256: "8b1f37dfaf6e958c6b872322db06f946509433bec3de753c3491a42ae9ec2b48"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "16.0.0" version: "16.1.0"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1153,10 +1201,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.5.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@@ -1193,10 +1241,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker_android name: image_picker_android
sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.12+24" version: "0.8.12+25"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@@ -1361,18 +1409,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_android name: local_auth_android
sha256: "82b2bdeee2199a510d3b7716121e96a6609da86693bb0863edd8566355406b79" sha256: "316503f6772dea9c0c038bb7aac4f68ab00112d707d258c770f7fc3c250a2d88"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.50" version: "1.0.51"
local_auth_darwin: local_auth_darwin:
dependency: transitive dependency: transitive
description: description:
name: local_auth_darwin name: local_auth_darwin
sha256: "25163ce60a5a6c468cf7a0e3dc8a165f824cabc2aa9e39a5e9fc5c2311b7686f" sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.6.0"
local_auth_platform_interface: local_auth_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1438,7 +1486,7 @@ packages:
source: hosted source: hosted
version: "0.12.17" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: "direct main"
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
@@ -1549,6 +1597,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
msix:
dependency: "direct dev"
description:
name: msix
sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5
url: "https://pub.dev"
source: hosted
version: "3.16.12"
native_exif: native_exif:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1593,18 +1649,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.3.0" version: "8.3.1"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
palette_generator: palette_generator:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1777,10 +1833,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: protobuf name: protobuf
sha256: "6153efcc92a06910918f3db8231fd2cf828ac81e50ebd87adc8f8a8cb3caff0e" sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "4.2.0"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@@ -1989,6 +2045,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
screenshot:
dependency: "direct main"
description:
name: screenshot
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
scroll_to_index: scroll_to_index:
dependency: transitive dependency: transitive
description: description:
@@ -2009,18 +2073,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.0.0" version: "11.1.0"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: share_plus_platform_interface name: share_plus_platform_interface
sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.1.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -2033,10 +2097,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.10" version: "2.4.11"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -2142,10 +2206,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_helper name: source_helper
sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1" sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.6" version: "1.3.7"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -2206,18 +2270,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlite3 name: sqlite3
sha256: dd806fff004a0aeb01e208b858dbc649bc72104670d425a81a6dd17698535f6e sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.8.0" version: "2.9.0"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: transitive dependency: transitive
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: fd996da5515a73aacd0a04ae7063db5fe8df42670d974df4c3ee538c652eef2e sha256: "2b03273e71867a8a4d030861fc21706200debe5c5858a4b9e58f4a1c129586a4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.38" version: "0.5.39"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
@@ -2424,10 +2488,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.16" version: "6.3.17"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -2654,4 +2718,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.8.0 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.32.0"

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 3.1.0+118 version: 3.2.0+124
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2
@@ -36,15 +36,15 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_hooks: ^0.21.2 flutter_hooks: ^0.21.3
hooks_riverpod: ^2.6.1 hooks_riverpod: ^2.6.1
bitsdojo_window: ^0.1.6 bitsdojo_window: ^0.1.6
go_router: ^16.0.0 go_router: ^16.1.0
styled_widget: ^0.4.1 styled_widget: ^0.4.1
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1 flutter_riverpod: ^2.6.1
path_provider: ^2.1.5 path_provider: ^2.1.5
dio: ^5.8.0+1 dio: ^5.9.0
very_good_infinite_list: ^0.9.0 very_good_infinite_list: ^0.9.0
freezed_annotation: ^3.1.0 freezed_annotation: ^3.1.0
json_annotation: ^4.9.0 json_annotation: ^4.9.0
@@ -67,16 +67,16 @@ dependencies:
easy_localization: ^3.0.8 easy_localization: ^3.0.8
flutter_inappwebview: ^6.1.5 flutter_inappwebview: ^6.1.5
animations: ^2.0.11 animations: ^2.0.11
package_info_plus: ^8.3.0 package_info_plus: ^8.3.1
device_info_plus: ^11.5.0 device_info_plus: ^11.5.0
tus_client_dart: tus_client_dart:
git: https://github.com/LittleSheep2Code/tus_client.git git: https://github.com/LittleSheep2Code/tus_client.git
cross_file: ^0.3.4+2 cross_file: ^0.3.4+2
image_picker: ^1.1.2 image_picker: ^1.1.2
file_picker: ^10.2.1 file_picker: ^10.3.1
riverpod_annotation: ^2.6.1 riverpod_annotation: ^2.6.1
image_picker_platform_interface: ^2.10.1 image_picker_platform_interface: ^2.10.1
image_picker_android: ^0.8.12+24 image_picker_android: ^0.8.12+25
super_context_menu: ^0.9.1 super_context_menu: ^0.9.1
modal_bottom_sheet: ^3.0.0 modal_bottom_sheet: ^3.0.0
firebase_messaging: ^16.0.0 firebase_messaging: ^16.0.0
@@ -121,7 +121,7 @@ dependencies:
local_auth: ^2.3.0 local_auth: ^2.3.0
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
flutter_math_fork: ^0.7.4 flutter_math_fork: ^0.7.4
share_plus: ^11.0.0 share_plus: ^11.1.0
receive_sharing_intent: ^1.8.1 receive_sharing_intent: ^1.8.1
top_snackbar_flutter: ^3.3.0 top_snackbar_flutter: ^3.3.0
textfield_tags: textfield_tags:
@@ -133,6 +133,11 @@ dependencies:
flutter_typeahead: ^5.2.0 flutter_typeahead: ^5.2.0
flutter_langdetect: ^0.0.2 flutter_langdetect: ^0.0.2
waveform_flutter: ^1.2.0 waveform_flutter: ^1.2.0
flutter_app_update: ^3.2.2
firebase_crashlytics: ^5.0.0
firebase_analytics: ^12.0.0
material_color_utilities: ^0.11.1
screenshot: ^3.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -144,7 +149,6 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
auto_route_generator: ^10.1.0
build_runner: ^2.5.4 build_runner: ^2.5.4
freezed: ^3.1.0 freezed: ^3.1.0
json_serializable: ^6.9.5 json_serializable: ^6.9.5
@@ -153,6 +157,7 @@ dev_dependencies:
riverpod_lint: ^2.6.5 riverpod_lint: ^2.6.5
drift_dev: ^2.28.0 drift_dev: ^2.28.0
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
msix: ^3.16.12
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec
@@ -222,3 +227,11 @@ flutter_native_splash:
image_dark: "assets/icons/icon-dark.png" image_dark: "assets/icons/icon-dark.png"
color: "#ffffff" color: "#ffffff"
color_dark: "#121212" color_dark: "#121212"
msix_config:
display_name: Solian
publisher_display_name: Solsynth LLC
identity_name: dev.solian.app
msix_version: 3.2.0.0
logo_path: .\assets\icons\icon.png
capabilities: internetClientServer, location, microphone, webcam

52
setup.iss Normal file
View File

@@ -0,0 +1,52 @@
; ==================================================
#define AppVersion "3.2.0"
#define BuildNumber "124"
; ==================================================
#define FullVersion AppVersion + "." + BuildNumber
[Setup]
AppName=Solian
AppVersion={#AppVersion}
AppPublisher=Solsynth
AppPublisherURL=https://solsynth.dev
AppSupportURL=https://kb.solsynth.dev/zh/solar-network
AppUpdatesURL=https://github.com/Solsynth/Solian/releases
AppCopyright=Copyright © 2025 Solsynth
VersionInfoVersion={#FullVersion}
UninstallDisplayName=Solian
UninstallDisplayIcon={app}\Solian.exe
DefaultDirName={commonpf}\Solian
UsePreviousAppDir=no
OutputDir=.\Installer
OutputBaseFilename=windows-x86_64-setup
SetupIconFile=.\assets\icons\icon.ico
Compression=lzma2/ultra64
SolidCompression=yes
LZMAUseSeparateProcess=yes
LZMANumBlockThreads=4
ArchitecturesAllowed=x64compatible
PrivilegesRequired=admin
[Files]
Source: ".\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\Solian"; Filename: "{app}\Solian.exe";IconFilename: "{app}\Solian.exe"
Name: "{group}\{cm:UninstallProgram,Solian}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\Solian"; Filename: "{app}\Solian.exe"; Tasks: desktopicon
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Run]
Filename: "{app}\Solian.exe"; Description: "Launch Solian"; Flags: nowait postinstall skipifsilent
[UninstallDelete]
Type: filesandordirs; Name: "{userappdata}\dev.solsynth\Solian"
Type: files; Name: "{group}\Solian.lnk" ;
Type: files; Name: "{autodesktop}\Solian.lnk" ;