Compare commits
	
		
			63 Commits
		
	
	
		
			3.1.0+118
			...
			001897bbcd
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 001897bbcd | |||
| 76b39f2df3 | |||
| 509b3e145b | |||
| 2b80ebc2d0 | |||
| 0ab908dd2a | |||
| 6007467e7a | |||
| 3745157c42 | |||
| 94481ec7bd | |||
| fbfe8cbdee | |||
| fbbab0a981 | |||
| ae2fb3b303 | |||
| 3d7a4666ed | |||
| 5d3e0fb800 | |||
| 85ff52a661 | |||
| da7fd64a43 | |||
| 3902633217 | |||
| f478ea8b84 | |||
| 0f481aff5b | |||
| 7a31663310 | |||
| 0239c53c04 | |||
| 16987c758e | |||
| 3a36915140 | |||
| 4bde708878 | |||
| 2f0cf560f8 | |||
| cf355a95fd | |||
| 2f43073172 | |||
| 8236d31ecc | |||
| 459a7dade0 | |||
| e6000a660a | |||
| 75abaac205 | |||
| 603d5c3f73 | |||
| 4e4bd99598 | |||
| d1fbe5f15e | |||
| c061ef2132 | |||
| c378309bdd | |||
| b2c5d64fc5 | |||
| 
						 | 
					5371637b16 | ||
| c5cbf0af37 | |||
| 1a31e22450 | |||
| 
						 | 
					49db54529d | ||
| 8e0c0c6054 | |||
| f3d1183076 | |||
| a9f7f0cce0 | |||
| f2943f8411 | |||
| 808e7dcffa | |||
| 9bed4fa6fb | |||
| e6255a340b | |||
| 78bf319fb7 | |||
| 36a966d582 | |||
| f72b268d36 | |||
| 44ef31034e | |||
| 229dc2186f | |||
| a2f9a1efb4 | |||
| 
						 | 
					823e3c5de6 | ||
| 
						 | 
					faac7bac35 | ||
| 1fac1bfe02 | |||
| 9394b1d9c8 | |||
| 43dd13bac4 | |||
| 65bc372103 | |||
| 6558854a7a | |||
| 892035ab27 | |||
| 87ae8d2ff4 | |||
| 15c2dbaa0d | 
							
								
								
									
										9
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@@ -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
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -12,6 +12,9 @@
 | 
				
			|||||||
.swiftpm/
 | 
					.swiftpm/
 | 
				
			||||||
migrate_working_dir/
 | 
					migrate_working_dir/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Inno Setup
 | 
				
			||||||
 | 
					Installer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# IntelliJ related
 | 
					# IntelliJ related
 | 
				
			||||||
*.iml
 | 
					*.iml
 | 
				
			||||||
*.ipr
 | 
					*.ipr
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
								
							
							
						
						
									
										5
									
								
								android/app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal 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();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/ic_notification.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/ic_notification.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 70 KiB  | 
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -334,6 +334,7 @@
 | 
				
			|||||||
  "walletCreate": "Create a Wallet",
 | 
					  "walletCreate": "Create a Wallet",
 | 
				
			||||||
  "settingsServerUrl": "Server URL",
 | 
					  "settingsServerUrl": "Server URL",
 | 
				
			||||||
  "settingsApplied": "The settings has been applied.",
 | 
					  "settingsApplied": "The settings has been applied.",
 | 
				
			||||||
 | 
					  "settingsCustomFontsHelper": "Use comma to seprate.",
 | 
				
			||||||
  "notifications": "Notifications",
 | 
					  "notifications": "Notifications",
 | 
				
			||||||
  "posts": "Posts",
 | 
					  "posts": "Posts",
 | 
				
			||||||
  "settingsBackgroundImage": "Background Image",
 | 
					  "settingsBackgroundImage": "Background Image",
 | 
				
			||||||
@@ -573,6 +574,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 +708,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 +762,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 +791,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)"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -46,7 +46,6 @@
 | 
				
			|||||||
  "delete": "删除",
 | 
					  "delete": "删除",
 | 
				
			||||||
  "deletePublisher": "删除发布者",
 | 
					  "deletePublisher": "删除发布者",
 | 
				
			||||||
  "deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。",
 | 
					  "deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。",
 | 
				
			||||||
  "somethingWentWrong": "发生了一些错误",
 | 
					 | 
				
			||||||
  "deletePost": "删除帖子",
 | 
					  "deletePost": "删除帖子",
 | 
				
			||||||
  "deletePostHint": "确定要删除这篇帖子吗?",
 | 
					  "deletePostHint": "确定要删除这篇帖子吗?",
 | 
				
			||||||
  "copyLink": "复制链接",
 | 
					  "copyLink": "复制链接",
 | 
				
			||||||
@@ -120,14 +119,9 @@
 | 
				
			|||||||
    "other": "{}个附件"
 | 
					    "other": "{}个附件"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "edited": "已编辑",
 | 
					  "edited": "已编辑",
 | 
				
			||||||
  "editedAt": "编辑于 {}",
 | 
					 | 
				
			||||||
  "addVideo": "添加视频",
 | 
					  "addVideo": "添加视频",
 | 
				
			||||||
  "addPhoto": "添加照片",
 | 
					  "addPhoto": "添加照片",
 | 
				
			||||||
  "addFile": "添加文件",
 | 
					  "addFile": "添加文件",
 | 
				
			||||||
  "addAttachmentById": "通过 ID 添加附件",
 | 
					 | 
				
			||||||
  "enterFileId": "输入文件 ID",
 | 
					 | 
				
			||||||
  "fileIdCannotBeEmpty": "文件 ID 不能为空",
 | 
					 | 
				
			||||||
  "failedToFetchFile": "获取文件失败: {}",
 | 
					 | 
				
			||||||
  "createDirectMessage": "创建新私人消息",
 | 
					  "createDirectMessage": "创建新私人消息",
 | 
				
			||||||
  "gotoDirectMessage": "前往私信",
 | 
					  "gotoDirectMessage": "前往私信",
 | 
				
			||||||
  "react": "反应",
 | 
					  "react": "反应",
 | 
				
			||||||
@@ -306,6 +300,7 @@
 | 
				
			|||||||
  "walletCreate": "创建钱包",
 | 
					  "walletCreate": "创建钱包",
 | 
				
			||||||
  "settingsServerUrl": "服务器 URL",
 | 
					  "settingsServerUrl": "服务器 URL",
 | 
				
			||||||
  "settingsApplied": "设置已应用。",
 | 
					  "settingsApplied": "设置已应用。",
 | 
				
			||||||
 | 
					  "settingsCustomFontsHelper": "用逗号分隔。",
 | 
				
			||||||
  "notifications": "通知",
 | 
					  "notifications": "通知",
 | 
				
			||||||
  "posts": "帖子",
 | 
					  "posts": "帖子",
 | 
				
			||||||
  "settingsBackgroundImage": "背景图片",
 | 
					  "settingsBackgroundImage": "背景图片",
 | 
				
			||||||
@@ -350,11 +345,10 @@
 | 
				
			|||||||
  "accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
 | 
					  "accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
 | 
				
			||||||
  "unauthorized": "未授权",
 | 
					  "unauthorized": "未授权",
 | 
				
			||||||
  "unauthorizedHint": "您未登录或会话已过期,请重新登录。",
 | 
					  "unauthorizedHint": "您未登录或会话已过期,请重新登录。",
 | 
				
			||||||
  "publisherBelongsTo": "属于 {}",
 | 
					  "publisherBelongsTo": "属于",
 | 
				
			||||||
  "postContent": "内容",
 | 
					  "postContent": "内容",
 | 
				
			||||||
  "postSettings": "设置",
 | 
					  "postSettings": "设置",
 | 
				
			||||||
  "postPublisherUnselected": "未指定发布者",
 | 
					  "postPublisherUnselected": "未指定发布者",
 | 
				
			||||||
  "postVisibility": "可见性",
 | 
					 | 
				
			||||||
  "postVisibilityPublic": "公开",
 | 
					  "postVisibilityPublic": "公开",
 | 
				
			||||||
  "postVisibilityFriends": "仅好友可见",
 | 
					  "postVisibilityFriends": "仅好友可见",
 | 
				
			||||||
  "postVisibilityUnlisted": "不公开",
 | 
					  "postVisibilityUnlisted": "不公开",
 | 
				
			||||||
@@ -478,7 +472,7 @@
 | 
				
			|||||||
  "description": "描述",
 | 
					  "description": "描述",
 | 
				
			||||||
  "pinCode": "PIN 码",
 | 
					  "pinCode": "PIN 码",
 | 
				
			||||||
  "biometric": "生物识别",
 | 
					  "biometric": "生物识别",
 | 
				
			||||||
  "enterPinToConfirm": "请输入您的 6 位数字 PIN 以确认付款",
 | 
					  "enterPinToConfirm": "请输入您的6位数字 PIN 以确认付款",
 | 
				
			||||||
  "clearPin": "清除 PIN 码",
 | 
					  "clearPin": "清除 PIN 码",
 | 
				
			||||||
  "useBiometricToConfirm": "使用生物特征认证来确认付款",
 | 
					  "useBiometricToConfirm": "使用生物特征认证来确认付款",
 | 
				
			||||||
  "touchSensorToAuthenticate": "触摸传感器进行身份验证",
 | 
					  "touchSensorToAuthenticate": "触摸传感器进行身份验证",
 | 
				
			||||||
@@ -495,20 +489,26 @@
 | 
				
			|||||||
  "paymentError": "付款失败: {error}",
 | 
					  "paymentError": "付款失败: {error}",
 | 
				
			||||||
  "usePinInstead": "使用 PIN 码",
 | 
					  "usePinInstead": "使用 PIN 码",
 | 
				
			||||||
  "levelProgress": "等级进度",
 | 
					  "levelProgress": "等级进度",
 | 
				
			||||||
 | 
					  "unlockedFeatures": "已解锁的功能",
 | 
				
			||||||
 | 
					  "unlockedFeaturesDescription": "在您当前级别上解锁的功能将显示在这里。",
 | 
				
			||||||
  "stellarMembership": "恒星计划",
 | 
					  "stellarMembership": "恒星计划",
 | 
				
			||||||
  "upgradeYourPlan": "升级您的计划",
 | 
					  "upgradeYourPlan": "升级您的计划",
 | 
				
			||||||
  "chooseYourPlan": "选择你的方案",
 | 
					  "chooseYourPlan": "选择你的方案",
 | 
				
			||||||
  "currentMembership": "当前:{}",
 | 
					  "currentMembership": "当前:{}",
 | 
				
			||||||
  "currentMembershipMember": "恒星计划「{}」级会员",
 | 
					 | 
				
			||||||
  "membershipExpires": "过期于:{}",
 | 
					  "membershipExpires": "过期于:{}",
 | 
				
			||||||
  "membershipTierStellar": "恒星",
 | 
					  "membershipTierStellar": "恒星",
 | 
				
			||||||
  "membershipTierNova": "新星",
 | 
					  "membershipTierNova": "新星",
 | 
				
			||||||
  "membershipTierSupernova": "超新星",
 | 
					  "membershipTierSupernova": "超新星",
 | 
				
			||||||
  "membershipTierUnknown": "未知",
 | 
					  "membershipTierUnknown": "未知",
 | 
				
			||||||
  "membershipPriceStellar": "每月 1200 源点,至少需要 3 级",
 | 
					 | 
				
			||||||
  "membershipPriceNova": "每月 2400 源点,至少需要 6 级",
 | 
					 | 
				
			||||||
  "membershipPriceSupernova": "每月 3600 源点,至少需要 9 级",
 | 
					 | 
				
			||||||
  "membershipFeatureBasic": "基础功能",
 | 
					  "membershipFeatureBasic": "基础功能",
 | 
				
			||||||
 | 
					  "membershipFeaturePrioritySupport": "优先支持",
 | 
				
			||||||
 | 
					  "membershipFeatureAdFree": "无广告",
 | 
				
			||||||
 | 
					  "membershipFeatureAllPrimary": "所有主要功能",
 | 
				
			||||||
 | 
					  "membershipFeatureAdvancedCustomization": "高级自定义",
 | 
				
			||||||
 | 
					  "membershipFeatureEarlyAccess": "抢先体验",
 | 
				
			||||||
 | 
					  "membershipFeatureAllNova": "所有「新星」功能",
 | 
				
			||||||
 | 
					  "membershipFeatureExclusiveContent": "限定内容",
 | 
				
			||||||
 | 
					  "membershipFeatureVipSupport": "VIP 支持",
 | 
				
			||||||
  "membershipCurrentBadge": "当前",
 | 
					  "membershipCurrentBadge": "当前",
 | 
				
			||||||
  "restorePurchase": "恢复购买",
 | 
					  "restorePurchase": "恢复购买",
 | 
				
			||||||
  "restorePurchaseDescription": "输入您付款的提供商和订单 ID 以恢复您的购买。",
 | 
					  "restorePurchaseDescription": "输入您付款的提供商和订单 ID 以恢复您的购买。",
 | 
				
			||||||
@@ -518,11 +518,186 @@
 | 
				
			|||||||
  "enterOrderId": "输入您的订单 ID",
 | 
					  "enterOrderId": "输入您的订单 ID",
 | 
				
			||||||
  "restore": "恢复",
 | 
					  "restore": "恢复",
 | 
				
			||||||
  "keyboardShortcuts": "键盘快捷键",
 | 
					  "keyboardShortcuts": "键盘快捷键",
 | 
				
			||||||
 | 
					  "safetyReport": "举报",
 | 
				
			||||||
 | 
					  "safetyReportTitle": "举报",
 | 
				
			||||||
 | 
					  "safetyReportDescription": "通过举报不合适的内容和行为来维护我们社区的稳定。",
 | 
				
			||||||
 | 
					  "safetyReportType": "举报类型",
 | 
				
			||||||
 | 
					  "safetyReportReason": "更多证据",
 | 
				
			||||||
 | 
					  "safetyReportReasonHint": "请提供更多证据……",
 | 
				
			||||||
 | 
					  "safetyReportSubmit": "提交举报",
 | 
				
			||||||
 | 
					  "safetyReportSubmitting": "提交中……",
 | 
				
			||||||
 | 
					  "safetyReportSuccess": "举报成功,感谢您参与维护社区健康发展。",
 | 
				
			||||||
 | 
					  "safetyReportError": "举报失败,请稍后重试。",
 | 
				
			||||||
 | 
					  "safetyReportReasonRequired": "请提供举报证据",
 | 
				
			||||||
 | 
					  "safetyReportTypeSpam": "垃圾或导向错误",
 | 
				
			||||||
 | 
					  "safetyReportTypeHarassment": "骚扰或暴力行为",
 | 
				
			||||||
 | 
					  "safetyReportTypeHateSpeech": "歧视言论",
 | 
				
			||||||
 | 
					  "safetyReportTypeViolence": "威胁或暴力内容",
 | 
				
			||||||
 | 
					  "safetyReportTypeAdultContent": "成人内容",
 | 
				
			||||||
 | 
					  "safetyReportTypeIntellectualProperty": "抄袭",
 | 
				
			||||||
 | 
					  "safetyReportTypeOther": "其它",
 | 
				
			||||||
 | 
					  "safetyReportTypeInappropriate": "不良内容",
 | 
				
			||||||
 | 
					  "safetyReportTypeCopyright": "版权侵害",
 | 
				
			||||||
 | 
					  "safetyReportSuccessTitle": "举报成功",
 | 
				
			||||||
 | 
					  "safetyReportErrorTitle": "错误",
 | 
				
			||||||
 | 
					  "discover": "发现",
 | 
				
			||||||
 | 
					  "joinRealm": "加入领域",
 | 
				
			||||||
 | 
					  "removePublisherMember": "移除发布者",
 | 
				
			||||||
 | 
					  "removePublisherMemberHint": "你确定要将这个成员从发布者中移除?",
 | 
				
			||||||
 | 
					  "drafts": "草稿箱",
 | 
				
			||||||
 | 
					  "noDrafts": "无草稿",
 | 
				
			||||||
 | 
					  "articleDrafts": "文章草稿",
 | 
				
			||||||
 | 
					  "postDrafts": "帖子草稿",
 | 
				
			||||||
 | 
					  "saveDraft": "保存草稿",
 | 
				
			||||||
 | 
					  "draftSaved": "草稿已保存",
 | 
				
			||||||
 | 
					  "draftSaveFailed": "保存草稿失败",
 | 
				
			||||||
 | 
					  "clearAllDrafts": "清除全部草稿",
 | 
				
			||||||
 | 
					  "clearAllDraftsConfirm": "你确定要清除全部草稿?这一操作无法撤销。",
 | 
				
			||||||
 | 
					  "clearAll": "清除所有",
 | 
				
			||||||
 | 
					  "untitled": "未命名",
 | 
				
			||||||
 | 
					  "noContent": "内容为空",
 | 
				
			||||||
 | 
					  "justNow": "刚刚",
 | 
				
			||||||
 | 
					  "minutesAgo": "{} 分钟以前",
 | 
				
			||||||
 | 
					  "hoursAgo": "{} 小时以前",
 | 
				
			||||||
 | 
					  "daysAgo": "{} 天以前",
 | 
				
			||||||
 | 
					  "public": "公开的",
 | 
				
			||||||
 | 
					  "unlisted": "不列出",
 | 
				
			||||||
 | 
					  "friends": "朋友",
 | 
				
			||||||
 | 
					  "selected": "选择的",
 | 
				
			||||||
 | 
					  "private": "私密的",
 | 
				
			||||||
 | 
					  "postContentEmpty": "发布的内容不能为空",
 | 
				
			||||||
 | 
					  "share": "分享",
 | 
				
			||||||
 | 
					  "sharePost": "分享帖子",
 | 
				
			||||||
 | 
					  "quickActions": "快捷操作",
 | 
				
			||||||
 | 
					  "post": "发帖",
 | 
				
			||||||
 | 
					  "copy": "复制",
 | 
				
			||||||
 | 
					  "sendToChat": "发送到聊天",
 | 
				
			||||||
 | 
					  "failedToShareToPost": "分享到帖子失败:{}",
 | 
				
			||||||
 | 
					  "shareToChatComingSoon": "分享到聊天功能即将推出",
 | 
				
			||||||
 | 
					  "failedToShareToChat": "分享到聊天失败:{}",
 | 
				
			||||||
 | 
					  "shareToSpecificChatComingSoon": "分享到 {} 功能即将推出",
 | 
				
			||||||
 | 
					  "directChat": "私信",
 | 
				
			||||||
 | 
					  "systemShareComingSoon": "系统分享功能即将推出",
 | 
				
			||||||
 | 
					  "failedToShareToSystem": "分享到系统失败:{}",
 | 
				
			||||||
 | 
					  "failedToCopy": "复制失败:{}",
 | 
				
			||||||
 | 
					  "noChatRoomsAvailable": "无可用聊天室",
 | 
				
			||||||
 | 
					  "failedToLoadChats": "加载聊天失败",
 | 
				
			||||||
 | 
					  "contentToShare": "分享内容:",
 | 
				
			||||||
 | 
					  "unknownChat": "未知聊天",
 | 
				
			||||||
 | 
					  "addAdditionalMessage": "添加附加消息……",
 | 
				
			||||||
 | 
					  "uploadingFiles": "上传文件中……",
 | 
				
			||||||
 | 
					  "sharedSuccessfully": "分享成功!",
 | 
				
			||||||
 | 
					  "shareSuccess": "分享成功!",
 | 
				
			||||||
 | 
					  "shareToSpecificChatSuccess": "成功分享至 {}!",
 | 
				
			||||||
 | 
					  "wouldYouLikeToGoToChat": "是否前往该聊天?",
 | 
				
			||||||
 | 
					  "no": "否",
 | 
				
			||||||
 | 
					  "yes": "是",
 | 
				
			||||||
 | 
					  "navigateToChat": "前往聊天",
 | 
				
			||||||
 | 
					  "abuseReport": "举报",
 | 
				
			||||||
 | 
					  "abuseReportTitle": "举报内容",
 | 
				
			||||||
 | 
					  "abuseReportDescription": "举报不当内容或行为,协助维护社区安全。",
 | 
				
			||||||
 | 
					  "abuseReportType": "举报类型",
 | 
				
			||||||
 | 
					  "abuseReportReason": "补充详情",
 | 
				
			||||||
 | 
					  "abuseReportReasonHint": "请提供更多详情……",
 | 
				
			||||||
 | 
					  "abuseReportSubmit": "提交举报",
 | 
				
			||||||
 | 
					  "abuseReportSuccess": "举报提交成功,感谢你为社区维护作出贡献。",
 | 
				
			||||||
 | 
					  "abuseReportError": "无法提交举报,请稍后再试。",
 | 
				
			||||||
 | 
					  "abuseReportReasonRequired": "请提供关于此事件的细节",
 | 
				
			||||||
 | 
					  "abuseReportSuccessTitle": "举报已提交",
 | 
				
			||||||
 | 
					  "abuseReportErrorTitle": "错误",
 | 
				
			||||||
 | 
					  "abuseReportTypeSpam": "垃圾或错误信息",
 | 
				
			||||||
 | 
					  "abuseReportTypeHarassment": "骚扰或滥用",
 | 
				
			||||||
 | 
					  "abuseReportTypeInappropriate": "不合适的内容",
 | 
				
			||||||
 | 
					  "abuseReportTypeViolence": "暴力或人身威胁",
 | 
				
			||||||
 | 
					  "abuseReportTypeCopyright": "版权侵犯",
 | 
				
			||||||
 | 
					  "abuseReportTypeImpersonation": "冒充",
 | 
				
			||||||
 | 
					  "abuseReportTypeOffensiveContent": "冒犯性内容",
 | 
				
			||||||
 | 
					  "abuseReportTypePrivacyViolation": "隐私侵犯",
 | 
				
			||||||
 | 
					  "abuseReportTypeIllegalContent": "违法内容",
 | 
				
			||||||
 | 
					  "abuseReportTypeOther": "其他",
 | 
				
			||||||
 | 
					  "tags": "标签",
 | 
				
			||||||
 | 
					  "tagsHint": "输入标签,用英文逗号分隔",
 | 
				
			||||||
 | 
					  "categories": "分类",
 | 
				
			||||||
 | 
					  "categoriesHint": "输入分类,由逗号隔开",
 | 
				
			||||||
 | 
					  "chatNotJoined": "你还没有加入这个聊天。",
 | 
				
			||||||
 | 
					  "chatUnableJoin": "由于该聊天的访问设置使你无法加入。",
 | 
				
			||||||
 | 
					  "chatJoin": "加入聊天",
 | 
				
			||||||
 | 
					  "realmJoin": "加入领域",
 | 
				
			||||||
 | 
					  "realmJoinSuccess": "成功加入领域。",
 | 
				
			||||||
 | 
					  "search": "搜索",
 | 
				
			||||||
 | 
					  "publisherMembers": "合作者",
 | 
				
			||||||
 | 
					  "developerHub": "开发者中心",
 | 
				
			||||||
 | 
					  "developerHubUnselectedHint": "选择一名开发者查看总结数据或成为一名。",
 | 
				
			||||||
 | 
					  "enrollDeveloper": "成为一名开发者",
 | 
				
			||||||
 | 
					  "enrollDeveloperHint": "让你的一个发布者成为开发者。",
 | 
				
			||||||
 | 
					  "noPublishersToEnroll": "你没有可以成为开发者的发布者。",
 | 
				
			||||||
 | 
					  "totalCustomApps": "所有应用套件",
 | 
				
			||||||
 | 
					  "customApps": "应用套件",
 | 
				
			||||||
 | 
					  "noCustomApps": "还没有应用套件。",
 | 
				
			||||||
 | 
					  "createCustomApp": "创建应用套件",
 | 
				
			||||||
 | 
					  "editCustomApp": "编辑应用套件",
 | 
				
			||||||
 | 
					  "deleteCustomApp": "删除应用套件",
 | 
				
			||||||
 | 
					  "deleteCustomAppHint": "你确定要删除这个应用套件吗?这一步无法撤销。",
 | 
				
			||||||
 | 
					  "publicRealm": "公开领域",
 | 
				
			||||||
 | 
					  "publicRealmDescription": "所有人都可以预览这个领域的内容。",
 | 
				
			||||||
 | 
					  "communityRealm": "领域",
 | 
				
			||||||
 | 
					  "communityRealmDescription": "所有人都可以加入该领域并参与讨论,并将在发现和反馈页面显示。",
 | 
				
			||||||
 | 
					  "publicChat": "公开聊天",
 | 
				
			||||||
 | 
					  "publicChatDescription": "任何人都可以预览此聊天的内容。包括未加入的机器人。",
 | 
				
			||||||
 | 
					  "communityChat": "社区聊天",
 | 
				
			||||||
 | 
					  "communityChatDescription": "所有人都可以加入该聊天并参与参与讨论。",
 | 
				
			||||||
 | 
					  "appLinks": "应用链接",
 | 
				
			||||||
 | 
					  "homePageUrl": "主页链接",
 | 
				
			||||||
 | 
					  "privacyPolicyUrl": "隐私政策链接",
 | 
				
			||||||
 | 
					  "termsOfServiceUrl": "用户协议链接",
 | 
				
			||||||
 | 
					  "oauthConfig": "OAuth 配置",
 | 
				
			||||||
 | 
					  "clientUri": "客户端 URI",
 | 
				
			||||||
 | 
					  "redirectUris": "重定向 URIs",
 | 
				
			||||||
 | 
					  "addRedirectUri": "添加重定向 URI",
 | 
				
			||||||
 | 
					  "allowedScopes": "允许的范围",
 | 
				
			||||||
 | 
					  "requirePkce": "需要 PKCE",
 | 
				
			||||||
 | 
					  "allowOfflineAccess": "允许离线访问",
 | 
				
			||||||
 | 
					  "redirectUri": "重定向 URI",
 | 
				
			||||||
 | 
					  "redirectUriHint": "重定向 URI 用于 OAuth 认证,但您的项目状态转为线上时我们会验证请求中的重定向 URI 是否符合此配置。",
 | 
				
			||||||
 | 
					  "uriRequired": "这个 URI 是必须填写的。",
 | 
				
			||||||
 | 
					  "uriInvalid": "无效 URI。",
 | 
				
			||||||
 | 
					  "add": "添加",
 | 
				
			||||||
 | 
					  "addScope": "添加范围",
 | 
				
			||||||
 | 
					  "scope": "范围",
 | 
				
			||||||
 | 
					  "publisherFeatures": "功能",
 | 
				
			||||||
 | 
					  "publisherFeatureDevelop": "开发者计划",
 | 
				
			||||||
 | 
					  "publisherFeatureDevelopDescription": "为你的开发者解锁包括应用套件,API 及更多开发功能。",
 | 
				
			||||||
 | 
					  "publisherFeatureDevelopHint": "目前该功能还在开发中,你需要邀请才可解锁。",
 | 
				
			||||||
 | 
					  "learnMore": "了解更多",
 | 
				
			||||||
 | 
					  "discoverWebArticles": "来自站外的文章",
 | 
				
			||||||
 | 
					  "webArticlesStand": "文章亭",
 | 
				
			||||||
  "about": "关于",
 | 
					  "about": "关于",
 | 
				
			||||||
 | 
					  "somethingWentWrong": "发生了一些错误",
 | 
				
			||||||
 | 
					  "editedAt": "编辑于 {}",
 | 
				
			||||||
 | 
					  "addAudio": "添加音频",
 | 
				
			||||||
 | 
					  "recordAudio": "录制音频",
 | 
				
			||||||
 | 
					  "linkAttachment": "链接附件",
 | 
				
			||||||
 | 
					  "fileIdCannotBeEmpty": "文件 ID 不能为空",
 | 
				
			||||||
 | 
					  "fileIdLinkHint": "还没有上传到 Solar Network?点击此处打开 Solar Network Drive,自定义您的上传内容。",
 | 
				
			||||||
 | 
					  "failedToFetchFile": "获取文件失败:{}",
 | 
				
			||||||
 | 
					  "callLeave": "离开",
 | 
				
			||||||
 | 
					  "callEnd": "挂断通话",
 | 
				
			||||||
 | 
					  "postType": "帖子类型",
 | 
				
			||||||
 | 
					  "articleAttachmentHint": "附件必须上传并插入到文章主体中才能显示出来。",
 | 
				
			||||||
 | 
					  "postVisibility": "可见性",
 | 
				
			||||||
 | 
					  "currentMembershipMember": "恒星计划成员 · {}",
 | 
				
			||||||
 | 
					  "membershipPriceStellar": "需要用户等级 3+,每月价格 1200 NSP",
 | 
				
			||||||
 | 
					  "membershipPriceNova": "需要用户等级 6+,每月价格 2400 NSP",
 | 
				
			||||||
 | 
					  "membershipPriceSupernova": "需要用户等级 9+,每月价格 3600 NSP",
 | 
				
			||||||
 | 
					  "sharePostPhoto": "通过图片分享帖子",
 | 
				
			||||||
 | 
					  "wouldYouLikeToNavigateToChat": "你想要前往聊天页面吗?",
 | 
				
			||||||
 | 
					  "abuseReports": "举报",
 | 
				
			||||||
 | 
					  "discoverRealms": "发现领域",
 | 
				
			||||||
 | 
					  "discoverPublishers": "发现发布者",
 | 
				
			||||||
  "membershipCancel": "取消会员订阅",
 | 
					  "membershipCancel": "取消会员订阅",
 | 
				
			||||||
  "membershipCancelConfirm": "您确定要取消您的会员订阅?",
 | 
					  "membershipCancelConfirm": "你确定要取消会员订阅吗?",
 | 
				
			||||||
  "membershipCancelHint": "您确定要取消您的会员订阅吗?您将不会再被收费。您的会员资格将在当前计费周期结束前保持有效。并且您在当前订阅结束之前无法重新订阅。",
 | 
					  "membershipCancelHint": "你确定要取消会员订阅吗?你将不会再次被扣费。你的会员资格将在当前计费周期结束前保持有效。并且你将无法重新订阅,直到当前订阅结束。",
 | 
				
			||||||
  "membershipCancelSuccess": "您的会员订阅已成功取消。",
 | 
					  "membershipCancelSuccess": "你的会员订阅已成功取消。",
 | 
				
			||||||
  "aboutScreenTitle": "关于",
 | 
					  "aboutScreenTitle": "关于",
 | 
				
			||||||
  "aboutScreenVersionInfo": "版本 {} ({})",
 | 
					  "aboutScreenVersionInfo": "版本 {} ({})",
 | 
				
			||||||
  "aboutScreenAppInfoSectionTitle": "应用信息",
 | 
					  "aboutScreenAppInfoSectionTitle": "应用信息",
 | 
				
			||||||
@@ -532,18 +707,110 @@
 | 
				
			|||||||
  "aboutScreenLinksSectionTitle": "链接",
 | 
					  "aboutScreenLinksSectionTitle": "链接",
 | 
				
			||||||
  "aboutScreenPrivacyPolicyTitle": "隐私政策",
 | 
					  "aboutScreenPrivacyPolicyTitle": "隐私政策",
 | 
				
			||||||
  "aboutScreenTermsOfServiceTitle": "服务条款",
 | 
					  "aboutScreenTermsOfServiceTitle": "服务条款",
 | 
				
			||||||
  "aboutScreenOpenSourceLicensesTitle": "开源许可证",
 | 
					  "aboutScreenOpenSourceLicensesTitle": "开源许可",
 | 
				
			||||||
  "aboutScreenDeveloperSectionTitle": "开发者",
 | 
					  "aboutScreenDeveloperSectionTitle": "开发者",
 | 
				
			||||||
  "aboutScreenContactUsTitle": "联系我们",
 | 
					  "aboutScreenContactUsTitle": "联系我们",
 | 
				
			||||||
  "aboutScreenLicenseTitle": "许可证",
 | 
					  "aboutScreenLicenseTitle": "许可",
 | 
				
			||||||
  "aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
 | 
					  "aboutScreenLicenseContent": "无法翻译",
 | 
				
			||||||
  "aboutScreenCopyright": "版权所有 © 索尔辛茨 {}",
 | 
					  "aboutScreenCopyright": "版权所有 © Solsynth {}",
 | 
				
			||||||
  "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作",
 | 
					  "aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作",
 | 
				
			||||||
  "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}",
 | 
					  "aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}",
 | 
				
			||||||
  "copiedToClipboard": "已复制到剪贴板",
 | 
					  "copiedToClipboard": "已复制到剪贴板",
 | 
				
			||||||
  "copyToClipboardTooltip": "复制到剪贴板",
 | 
					  "copyToClipboardTooltip": "复制到剪贴板",
 | 
				
			||||||
  "postForwardingTo": "转发给",
 | 
					  "postForwardingTo": "正在转发到",
 | 
				
			||||||
  "postReplyingTo": "回复给",
 | 
					  "postReplyingTo": "正在回复",
 | 
				
			||||||
  "postEditing": "您正在编辑现有帖子",
 | 
					  "postReplyPlaceholder": "发表你的回复",
 | 
				
			||||||
  "postArticle": "文章"
 | 
					  "postEditing": "你正在编辑一个现有的帖子",
 | 
				
			||||||
 | 
					  "postArticle": "文章",
 | 
				
			||||||
 | 
					  "aboutDeviceName": "设备名称",
 | 
				
			||||||
 | 
					  "aboutDeviceIdentifier": "设备标识符",
 | 
				
			||||||
 | 
					  "donate": "捐赠",
 | 
				
			||||||
 | 
					  "donateDescription": "支持我们继续开发 Solar Network,并维持服务器运行。",
 | 
				
			||||||
 | 
					  "fileId": "文件 ID",
 | 
				
			||||||
 | 
					  "fileIdHint": "文件 ID 是你通过 Solar Network Drive 上传文件后获得的 ID。",
 | 
				
			||||||
 | 
					  "translate": "翻译",
 | 
				
			||||||
 | 
					  "translating": "正在翻译",
 | 
				
			||||||
 | 
					  "translated": "已翻译",
 | 
				
			||||||
 | 
					  "reactionThumbUp": "赞",
 | 
				
			||||||
 | 
					  "reactionThumbDown": "踩",
 | 
				
			||||||
 | 
					  "reactionJustOkay": "还行",
 | 
				
			||||||
 | 
					  "reactionCry": "哭",
 | 
				
			||||||
 | 
					  "reactionConfuse": "困惑",
 | 
				
			||||||
 | 
					  "reactionClap": "鼓掌",
 | 
				
			||||||
 | 
					  "reactionLaugh": "笑",
 | 
				
			||||||
 | 
					  "reactionAngry": "生气",
 | 
				
			||||||
 | 
					  "reactionParty": "派对",
 | 
				
			||||||
 | 
					  "reactionPray": "祈祷",
 | 
				
			||||||
 | 
					  "reactionHeart": "爱心",
 | 
				
			||||||
 | 
					  "selectMicrophone": "选择麦克风",
 | 
				
			||||||
 | 
					  "selectCamera": "选择摄像头",
 | 
				
			||||||
 | 
					  "switchedTo": "已切换到 {}",
 | 
				
			||||||
 | 
					  "connecting": "正在连接",
 | 
				
			||||||
 | 
					  "reconnecting": "正在重新连接",
 | 
				
			||||||
 | 
					  "disconnected": "已断开连接",
 | 
				
			||||||
 | 
					  "connected": "已连接",
 | 
				
			||||||
 | 
					  "repliesLoadMore": "加载更多回复",
 | 
				
			||||||
 | 
					  "attachmentsRecentUploads": "最近上传",
 | 
				
			||||||
 | 
					  "attachmentsManualInput": "手动输入",
 | 
				
			||||||
 | 
					  "crop": "裁剪",
 | 
				
			||||||
 | 
					  "rename": "重命名",
 | 
				
			||||||
 | 
					  "markAsSensitive": "标记为敏感",
 | 
				
			||||||
 | 
					  "fileName": "文件名",
 | 
				
			||||||
 | 
					  "sensitiveCategories": {
 | 
				
			||||||
 | 
					    "language": "语言",
 | 
				
			||||||
 | 
					    "sexualContent": "色情内容",
 | 
				
			||||||
 | 
					    "violence": "暴力",
 | 
				
			||||||
 | 
					    "profanity": "亵渎",
 | 
				
			||||||
 | 
					    "hateSpeech": "仇恨言论",
 | 
				
			||||||
 | 
					    "racism": "种族主义",
 | 
				
			||||||
 | 
					    "adultContent": "成人内容",
 | 
				
			||||||
 | 
					    "drugAbuse": "药物滥用",
 | 
				
			||||||
 | 
					    "alcoholAbuse": "酗酒",
 | 
				
			||||||
 | 
					    "gambling": "赌博",
 | 
				
			||||||
 | 
					    "selfHarm": "自残",
 | 
				
			||||||
 | 
					    "childAbuse": "虐待儿童",
 | 
				
			||||||
 | 
					    "other": "其他"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "poll": "投票",
 | 
				
			||||||
 | 
					  "pollsRecent": "最近投票",
 | 
				
			||||||
 | 
					  "pollCreateNew": "创建新投票",
 | 
				
			||||||
 | 
					  "pollCreateNewHint": "为你的帖子创建一个新投票。选择一个发布者然后继续。",
 | 
				
			||||||
 | 
					  "publisher": "发布者",
 | 
				
			||||||
 | 
					  "publisherHint": "输入发布者名称",
 | 
				
			||||||
 | 
					  "publisherCannotBeEmpty": "发布者不能为空",
 | 
				
			||||||
 | 
					  "operationFailed": "操作失败:{}",
 | 
				
			||||||
 | 
					  "stickerMarketplace": "贴纸市场",
 | 
				
			||||||
 | 
					  "stickerPackAdded": "贴纸包已添加到你的收藏",
 | 
				
			||||||
 | 
					  "stickerPackRemoved": "贴纸包已从你的收藏中移除",
 | 
				
			||||||
 | 
					  "addPack": "添加贴纸包",
 | 
				
			||||||
 | 
					  "removePack": "移除贴纸包",
 | 
				
			||||||
 | 
					  "browseAndAddStickers": "浏览并添加贴纸包",
 | 
				
			||||||
 | 
					  "stickerPack": "贴纸包",
 | 
				
			||||||
 | 
					  "postCategoryTechnology": "科技",
 | 
				
			||||||
 | 
					  "postCategoryTravel": "旅行",
 | 
				
			||||||
 | 
					  "postCategoryFood": "美食",
 | 
				
			||||||
 | 
					  "postCategoryHealth": "健康",
 | 
				
			||||||
 | 
					  "postCategoryScience": "科学",
 | 
				
			||||||
 | 
					  "postCategorySports": "体育",
 | 
				
			||||||
 | 
					  "postCategoryFinance": "金融",
 | 
				
			||||||
 | 
					  "postCategoryLife": "生活",
 | 
				
			||||||
 | 
					  "postCategoryArt": "艺术",
 | 
				
			||||||
 | 
					  "postCategoryStudy": "学习",
 | 
				
			||||||
 | 
					  "postCategoryGaming": "游戏",
 | 
				
			||||||
 | 
					  "postCategoryProgramming": "编程",
 | 
				
			||||||
 | 
					  "postCategoryMusic": "音乐",
 | 
				
			||||||
 | 
					  "links": "链接",
 | 
				
			||||||
 | 
					  "addLink": "添加链接",
 | 
				
			||||||
 | 
					  "linkKey": "链接名称",
 | 
				
			||||||
 | 
					  "linkValue": "链接",
 | 
				
			||||||
 | 
					  "debugOptions": "调试选项",
 | 
				
			||||||
 | 
					  "joinedAt": "加入于 {}",
 | 
				
			||||||
 | 
					  "searchAccounts": "搜索帐号……",
 | 
				
			||||||
 | 
					  "webFeeds": "订阅源",
 | 
				
			||||||
 | 
					  "polls": "投票",
 | 
				
			||||||
 | 
					  "sharePostSlogan": "加入 Solar Network 以便探索更多",
 | 
				
			||||||
 | 
					  "filesListAdditional": {
 | 
				
			||||||
 | 
					    "one": "+{} 个文件被折叠",
 | 
				
			||||||
 | 
					    "other": "+{} 个文件被折叠"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 108 KiB  | 
							
								
								
									
										139
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							@@ -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,9 +241,11 @@ 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.1.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - SAMKeychain (1.5.3)
 | 
					  - SAMKeychain (1.5.3)
 | 
				
			||||||
  - SDWebImage (5.21.1):
 | 
					  - SDWebImage (5.21.1):
 | 
				
			||||||
@@ -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,16 +508,17 @@ 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: f75fa1d57f840012775c0e93a38a7f3ceea1a374
 | 
				
			||||||
  SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
 | 
					  SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
 | 
				
			||||||
  SDWebImage: f29024626962457f3470184232766516dee8dfea
 | 
					  SDWebImage: f29024626962457f3470184232766516dee8dfea
 | 
				
			||||||
  share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
 | 
					  share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
 | 
				
			||||||
  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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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',
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
@@ -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,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,25 @@
 | 
				
			|||||||
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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,8 +11,8 @@ sealed class SnScrappedLink with _$SnScrappedLink {
 | 
				
			|||||||
    required String title,
 | 
					    required String title,
 | 
				
			||||||
    required String? description,
 | 
					    required String? description,
 | 
				
			||||||
    required String? imageUrl,
 | 
					    required String? imageUrl,
 | 
				
			||||||
    required String faviconUrl,
 | 
					    required String? faviconUrl,
 | 
				
			||||||
    required String siteName,
 | 
					    required String? siteName,
 | 
				
			||||||
    required String? contentType,
 | 
					    required String? contentType,
 | 
				
			||||||
    required String? author,
 | 
					    required String? author,
 | 
				
			||||||
    required DateTime? publishedDate,
 | 
					    required DateTime? publishedDate,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
 | 
				
			|||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$SnScrappedLink {
 | 
					mixin _$SnScrappedLink {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 String get type; String get url; String get title; String? get description; String? get imageUrl; String get faviconUrl; String get siteName; String? get contentType; String? get author; DateTime? get publishedDate;
 | 
					 String get type; String get url; String get title; String? get description; String? get imageUrl; String? get faviconUrl; String? get siteName; String? get contentType; String? get author; DateTime? get publishedDate;
 | 
				
			||||||
/// Create a copy of SnScrappedLink
 | 
					/// Create a copy of SnScrappedLink
 | 
				
			||||||
/// 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)
 | 
				
			||||||
@@ -48,7 +48,7 @@ abstract mixin class $SnScrappedLinkCopyWith<$Res>  {
 | 
				
			|||||||
  factory $SnScrappedLinkCopyWith(SnScrappedLink value, $Res Function(SnScrappedLink) _then) = _$SnScrappedLinkCopyWithImpl;
 | 
					  factory $SnScrappedLinkCopyWith(SnScrappedLink value, $Res Function(SnScrappedLink) _then) = _$SnScrappedLinkCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate
 | 
					 String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -65,16 +65,16 @@ class _$SnScrappedLinkCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of SnScrappedLink
 | 
					/// Create a copy of SnScrappedLink
 | 
				
			||||||
/// 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? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = freezed,Object? siteName = freezed,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
 | 
				
			||||||
  return _then(_self.copyWith(
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
					type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
 | 
					as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
					as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
					as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
 | 
					as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
 | 
					as String?,faviconUrl: freezed == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
 | 
					as String?,siteName: freezed == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
 | 
					as String?,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
 | 
					as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
 | 
					as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime?,
 | 
					as DateTime?,
 | 
				
			||||||
@@ -159,7 +159,7 @@ return $default(_that);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String faviconUrl,  String siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String? faviconUrl,  String? siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnScrappedLink() when $default != null:
 | 
					case _SnScrappedLink() when $default != null:
 | 
				
			||||||
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
 | 
					return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
 | 
				
			||||||
@@ -180,7 +180,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String faviconUrl,  String siteName,  String? contentType,  String? author,  DateTime? publishedDate)  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String? faviconUrl,  String? siteName,  String? contentType,  String? author,  DateTime? publishedDate)  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnScrappedLink():
 | 
					case _SnScrappedLink():
 | 
				
			||||||
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);}
 | 
					return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);}
 | 
				
			||||||
@@ -197,7 +197,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String faviconUrl,  String siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String? faviconUrl,  String? siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnScrappedLink() when $default != null:
 | 
					case _SnScrappedLink() when $default != null:
 | 
				
			||||||
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
 | 
					return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
 | 
				
			||||||
@@ -220,8 +220,8 @@ class _SnScrappedLink implements SnScrappedLink {
 | 
				
			|||||||
@override final  String title;
 | 
					@override final  String title;
 | 
				
			||||||
@override final  String? description;
 | 
					@override final  String? description;
 | 
				
			||||||
@override final  String? imageUrl;
 | 
					@override final  String? imageUrl;
 | 
				
			||||||
@override final  String faviconUrl;
 | 
					@override final  String? faviconUrl;
 | 
				
			||||||
@override final  String siteName;
 | 
					@override final  String? siteName;
 | 
				
			||||||
@override final  String? contentType;
 | 
					@override final  String? contentType;
 | 
				
			||||||
@override final  String? author;
 | 
					@override final  String? author;
 | 
				
			||||||
@override final  DateTime? publishedDate;
 | 
					@override final  DateTime? publishedDate;
 | 
				
			||||||
@@ -259,7 +259,7 @@ abstract mixin class _$SnScrappedLinkCopyWith<$Res> implements $SnScrappedLinkCo
 | 
				
			|||||||
  factory _$SnScrappedLinkCopyWith(_SnScrappedLink value, $Res Function(_SnScrappedLink) _then) = __$SnScrappedLinkCopyWithImpl;
 | 
					  factory _$SnScrappedLinkCopyWith(_SnScrappedLink value, $Res Function(_SnScrappedLink) _then) = __$SnScrappedLinkCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate
 | 
					 String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -276,16 +276,16 @@ class __$SnScrappedLinkCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of SnScrappedLink
 | 
					/// Create a copy of SnScrappedLink
 | 
				
			||||||
/// 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? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = freezed,Object? siteName = freezed,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
 | 
				
			||||||
  return _then(_SnScrappedLink(
 | 
					  return _then(_SnScrappedLink(
 | 
				
			||||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
					type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
 | 
					as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
					as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
					as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
 | 
					as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
 | 
					as String?,faviconUrl: freezed == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
 | 
					as String?,siteName: freezed == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
 | 
					as String?,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
 | 
					as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
 | 
					as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime?,
 | 
					as DateTime?,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,8 @@ _SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
      title: json['title'] as String,
 | 
					      title: json['title'] as String,
 | 
				
			||||||
      description: json['description'] as String?,
 | 
					      description: json['description'] as String?,
 | 
				
			||||||
      imageUrl: json['image_url'] as String?,
 | 
					      imageUrl: json['image_url'] as String?,
 | 
				
			||||||
      faviconUrl: json['favicon_url'] as String,
 | 
					      faviconUrl: json['favicon_url'] as String?,
 | 
				
			||||||
      siteName: json['site_name'] as String,
 | 
					      siteName: json['site_name'] as String?,
 | 
				
			||||||
      contentType: json['content_type'] as String?,
 | 
					      contentType: json['content_type'] as String?,
 | 
				
			||||||
      author: json['author'] as String?,
 | 
					      author: json['author'] as String?,
 | 
				
			||||||
      publishedDate:
 | 
					      publishedDate:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,8 @@ const kAppSoundEffects = 'app_sound_effects';
 | 
				
			|||||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
 | 
					const kAppAprilFoolFeatures = 'app_april_fool_features';
 | 
				
			||||||
const kAppWindowSize = 'app_window_size';
 | 
					const kAppWindowSize = 'app_window_size';
 | 
				
			||||||
const kAppEnterToSend = 'app_enter_to_send';
 | 
					const kAppEnterToSend = 'app_enter_to_send';
 | 
				
			||||||
 | 
					const kFeaturedPostsCollapsedId =
 | 
				
			||||||
 | 
					    'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Map<String, FilterQuality> kImageQualityLevel = {
 | 
					const Map<String, FilterQuality> kImageQualityLevel = {
 | 
				
			||||||
  'settingsImageQualityLowest': FilterQuality.none,
 | 
					  'settingsImageQualityLowest': FilterQuality.none,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					import 'package:animations/animations.dart';
 | 
				
			||||||
 | 
					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';
 | 
				
			||||||
@@ -54,11 +56,28 @@ final rootNavigatorKey = GlobalKey<NavigatorState>();
 | 
				
			|||||||
final _shellNavigatorKey = GlobalKey<NavigatorState>();
 | 
					final _shellNavigatorKey = GlobalKey<NavigatorState>();
 | 
				
			||||||
final _tabsShellKey = GlobalKey<NavigatorState>();
 | 
					final _tabsShellKey = GlobalKey<NavigatorState>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Widget _tabPagesTransitionBuilder(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  Animation<double> animation,
 | 
				
			||||||
 | 
					  Animation<double> secondaryAnimation,
 | 
				
			||||||
 | 
					  Widget child,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  return FadeThroughTransition(
 | 
				
			||||||
 | 
					    animation: animation,
 | 
				
			||||||
 | 
					    secondaryAnimation: secondaryAnimation,
 | 
				
			||||||
 | 
					    fillColor: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
 | 
					    child: child,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Provider for the router
 | 
					// Provider for the router
 | 
				
			||||||
final routerProvider = Provider<GoRouter>((ref) {
 | 
					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,
 | 
				
			||||||
@@ -334,7 +353,12 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                name: 'explore',
 | 
					                name: 'explore',
 | 
				
			||||||
                path: '/',
 | 
					                path: '/',
 | 
				
			||||||
                builder: (context, state) => const ExploreScreen(),
 | 
					                pageBuilder:
 | 
				
			||||||
 | 
					                    (context, state) => CustomTransitionPage(
 | 
				
			||||||
 | 
					                      key: const ValueKey('explore'),
 | 
				
			||||||
 | 
					                      child: const ExploreScreen(),
 | 
				
			||||||
 | 
					                      transitionsBuilder: _tabPagesTransitionBuilder,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                name: 'postSearch',
 | 
					                name: 'postSearch',
 | 
				
			||||||
@@ -384,8 +408,12 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
              // Chat tab
 | 
					              // Chat tab
 | 
				
			||||||
              ShellRoute(
 | 
					              ShellRoute(
 | 
				
			||||||
                builder:
 | 
					                pageBuilder:
 | 
				
			||||||
                    (context, state, child) => ChatShellScreen(child: child),
 | 
					                    (context, state, child) => CustomTransitionPage(
 | 
				
			||||||
 | 
					                      key: const ValueKey('chat'),
 | 
				
			||||||
 | 
					                      child: ChatShellScreen(child: child),
 | 
				
			||||||
 | 
					                      transitionsBuilder: _tabPagesTransitionBuilder,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                routes: [
 | 
					                routes: [
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    name: 'chatList',
 | 
					                    name: 'chatList',
 | 
				
			||||||
@@ -428,7 +456,12 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                name: 'realmList',
 | 
					                name: 'realmList',
 | 
				
			||||||
                path: '/realms',
 | 
					                path: '/realms',
 | 
				
			||||||
                builder: (context, state) => const RealmListScreen(),
 | 
					                pageBuilder:
 | 
				
			||||||
 | 
					                    (context, state) => CustomTransitionPage(
 | 
				
			||||||
 | 
					                      key: const ValueKey('realms'),
 | 
				
			||||||
 | 
					                      child: const RealmListScreen(),
 | 
				
			||||||
 | 
					                      transitionsBuilder: _tabPagesTransitionBuilder,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                routes: [
 | 
					                routes: [
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    name: 'realmNew',
 | 
					                    name: 'realmNew',
 | 
				
			||||||
@@ -456,8 +489,12 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
              // Account tab
 | 
					              // Account tab
 | 
				
			||||||
              ShellRoute(
 | 
					              ShellRoute(
 | 
				
			||||||
                builder:
 | 
					                pageBuilder:
 | 
				
			||||||
                    (context, state, child) => AccountShellScreen(child: child),
 | 
					                    (context, state, child) => CustomTransitionPage(
 | 
				
			||||||
 | 
					                      key: const ValueKey('account'),
 | 
				
			||||||
 | 
					                      child: AccountShellScreen(child: child),
 | 
				
			||||||
 | 
					                      transitionsBuilder: _tabPagesTransitionBuilder,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                routes: [
 | 
					                routes: [
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    name: 'account',
 | 
					                    name: 'account',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
@@ -178,7 +178,8 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
 | 
				
			|||||||
                                context,
 | 
					                                context,
 | 
				
			||||||
                                icon: Symbols.label,
 | 
					                                icon: Symbols.label,
 | 
				
			||||||
                                label: 'aboutDeviceName'.tr(),
 | 
					                                label: 'aboutDeviceName'.tr(),
 | 
				
			||||||
                                value: _deviceInfo?.data['name'],
 | 
					                                value:
 | 
				
			||||||
 | 
					                                    _deviceInfo?.data['name'] ?? 'unknown'.tr(),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                              _buildInfoItem(
 | 
					                              _buildInfoItem(
 | 
				
			||||||
                                context,
 | 
					                                context,
 | 
				
			||||||
@@ -205,33 +206,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.',
 | 
					 | 
				
			||||||
                                              ),
 | 
					 | 
				
			||||||
                                            ),
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                        ),
 | 
					 | 
				
			||||||
                                  );
 | 
					                                  );
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                              },
 | 
					                              },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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',
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,6 +580,7 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                              SliverToBoxAdapter(
 | 
					                              SliverToBoxAdapter(
 | 
				
			||||||
                                child: accountProfileBio(data).padding(top: 4),
 | 
					                                child: accountProfileBio(data).padding(top: 4),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
 | 
					                              if (data.profile.links.isNotEmpty)
 | 
				
			||||||
                                SliverToBoxAdapter(
 | 
					                                SliverToBoxAdapter(
 | 
				
			||||||
                                  child: accountProfileLinks(data),
 | 
					                                  child: accountProfileLinks(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,6 +680,7 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                        SliverToBoxAdapter(
 | 
					                        SliverToBoxAdapter(
 | 
				
			||||||
                          child: accountProfileBio(data).padding(horizontal: 4),
 | 
					                          child: accountProfileBio(data).padding(horizontal: 4),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
 | 
					                        if (data.profile.links.isNotEmpty)
 | 
				
			||||||
                          SliverToBoxAdapter(
 | 
					                          SliverToBoxAdapter(
 | 
				
			||||||
                            child: accountProfileLinks(
 | 
					                            child: accountProfileLinks(
 | 
				
			||||||
                              data,
 | 
					                              data,
 | 
				
			||||||
@@ -670,7 +691,7 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                            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),
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(),
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
					 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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'),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -151,7 +151,7 @@ class _StickerPackContentProviderElement
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$stickerPackStickerHash() =>
 | 
					String _$stickerPackStickerHash() =>
 | 
				
			||||||
    r'36f524c047e632236d5597aaaa8678ed86599602';
 | 
					    r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// See also [stickerPackSticker].
 | 
					/// See also [stickerPackSticker].
 | 
				
			||||||
@ProviderFor(stickerPackSticker)
 | 
					@ProviderFor(stickerPackSticker)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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')),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(),
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ import 'package:island/models/realm.dart';
 | 
				
			|||||||
import 'package:island/models/webfeed.dart';
 | 
					import 'package:island/models/webfeed.dart';
 | 
				
			||||||
import 'package:island/pods/event_calendar.dart';
 | 
					import 'package:island/pods/event_calendar.dart';
 | 
				
			||||||
import 'package:island/pods/userinfo.dart';
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/notification.dart';
 | 
				
			||||||
import 'package:island/services/responsive.dart';
 | 
					import 'package:island/services/responsive.dart';
 | 
				
			||||||
import 'package:island/widgets/account/fortune_graph.dart';
 | 
					import 'package:island/widgets/account/fortune_graph.dart';
 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
@@ -30,6 +31,33 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
part 'explore.g.dart';
 | 
					part 'explore.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Widget notificationIndicatorWidget(
 | 
				
			||||||
 | 
					  BuildContext context, {
 | 
				
			||||||
 | 
					  required int count,
 | 
				
			||||||
 | 
					  EdgeInsets? margin,
 | 
				
			||||||
 | 
					}) => Card(
 | 
				
			||||||
 | 
					  margin: margin,
 | 
				
			||||||
 | 
					  child: ListTile(
 | 
				
			||||||
 | 
					    shape: const RoundedRectangleBorder(
 | 
				
			||||||
 | 
					      borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    leading: const Icon(Symbols.notifications),
 | 
				
			||||||
 | 
					    title: Row(
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Text('notifications').tr().fontSize(14),
 | 
				
			||||||
 | 
					        const Gap(8),
 | 
				
			||||||
 | 
					        Badge(label: Text(count.toString())),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					    minTileHeight: 40,
 | 
				
			||||||
 | 
					    contentPadding: EdgeInsets.only(left: 16, right: 15),
 | 
				
			||||||
 | 
					    onTap: () {
 | 
				
			||||||
 | 
					      GoRouter.of(context).pushNamed('notifications');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExploreScreen extends HookConsumerWidget {
 | 
					class ExploreScreen extends HookConsumerWidget {
 | 
				
			||||||
  const ExploreScreen({super.key});
 | 
					  const ExploreScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -77,6 +105,10 @@ class ExploreScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final user = ref.watch(userInfoProvider);
 | 
					    final user = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final notificationCount = ref.watch(
 | 
				
			||||||
 | 
					      notificationUnreadCountNotifierProvider,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      isNoBackground: false,
 | 
					      isNoBackground: false,
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
@@ -200,6 +232,8 @@ class ExploreScreen extends HookConsumerWidget {
 | 
				
			|||||||
                if (user.value != null)
 | 
					                if (user.value != null)
 | 
				
			||||||
                  Flexible(
 | 
					                  Flexible(
 | 
				
			||||||
                    flex: 2,
 | 
					                    flex: 2,
 | 
				
			||||||
 | 
					                    child: Align(
 | 
				
			||||||
 | 
					                      alignment: Alignment.topCenter,
 | 
				
			||||||
                      child: SingleChildScrollView(
 | 
					                      child: SingleChildScrollView(
 | 
				
			||||||
                        child: Column(
 | 
					                        child: Column(
 | 
				
			||||||
                          children: [
 | 
					                          children: [
 | 
				
			||||||
@@ -215,13 +249,28 @@ class ExploreScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                );
 | 
					                                );
 | 
				
			||||||
                              },
 | 
					                              },
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
 | 
					                            if (notificationCount.value != null &&
 | 
				
			||||||
 | 
					                                notificationCount.value! > 0)
 | 
				
			||||||
 | 
					                              notificationIndicatorWidget(
 | 
				
			||||||
 | 
					                                context,
 | 
				
			||||||
 | 
					                                count: notificationCount.value ?? 0,
 | 
				
			||||||
 | 
					                                margin: EdgeInsets.only(
 | 
				
			||||||
 | 
					                                  left: 8,
 | 
				
			||||||
 | 
					                                  right: 12,
 | 
				
			||||||
 | 
					                                  top: 8,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
                            PostFeaturedList().padding(
 | 
					                            PostFeaturedList().padding(
 | 
				
			||||||
                              left: 8,
 | 
					                              left: 8,
 | 
				
			||||||
                              right: 12,
 | 
					                              right: 12,
 | 
				
			||||||
                              top: 8,
 | 
					                              top: 8,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                            FortuneGraphWidget(
 | 
					                            FortuneGraphWidget(
 | 
				
			||||||
                            margin: EdgeInsets.only(left: 8, right: 12, top: 8),
 | 
					                              margin: EdgeInsets.only(
 | 
				
			||||||
 | 
					                                left: 8,
 | 
				
			||||||
 | 
					                                right: 12,
 | 
				
			||||||
 | 
					                                top: 8,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
                              events: events,
 | 
					                              events: events,
 | 
				
			||||||
                              constrainWidth: true,
 | 
					                              constrainWidth: true,
 | 
				
			||||||
                              onPointSelected: onDaySelected,
 | 
					                              onPointSelected: onDaySelected,
 | 
				
			||||||
@@ -229,6 +278,7 @@ class ExploreScreen extends HookConsumerWidget {
 | 
				
			|||||||
                          ],
 | 
					                          ],
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                  )
 | 
					                  )
 | 
				
			||||||
                else
 | 
					                else
 | 
				
			||||||
                  Flexible(
 | 
					                  Flexible(
 | 
				
			||||||
@@ -380,6 +430,10 @@ class _ActivityListView extends HookConsumerWidget {
 | 
				
			|||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final user = ref.watch(userInfoProvider);
 | 
					    final user = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final notificationCount = ref.watch(
 | 
				
			||||||
 | 
					      notificationUnreadCountNotifierProvider,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return CustomScrollView(
 | 
					    return CustomScrollView(
 | 
				
			||||||
      slivers: [
 | 
					      slivers: [
 | 
				
			||||||
        SliverGap(12),
 | 
					        SliverGap(12),
 | 
				
			||||||
@@ -393,6 +447,14 @@ class _ActivityListView extends HookConsumerWidget {
 | 
				
			|||||||
          SliverToBoxAdapter(
 | 
					          SliverToBoxAdapter(
 | 
				
			||||||
            child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4),
 | 
					            child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					        if (!contentOnly)
 | 
				
			||||||
 | 
					          SliverToBoxAdapter(
 | 
				
			||||||
 | 
					            child: notificationIndicatorWidget(
 | 
				
			||||||
 | 
					              context,
 | 
				
			||||||
 | 
					              count: notificationCount.value ?? 0,
 | 
				
			||||||
 | 
					              margin: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
        SliverList.builder(
 | 
					        SliverList.builder(
 | 
				
			||||||
          itemCount: widgetCount,
 | 
					          itemCount: widgetCount,
 | 
				
			||||||
          itemBuilder: (context, index) {
 | 
					          itemBuilder: (context, index) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,17 @@ import 'dart:math' as math;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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: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';
 | 
				
			||||||
 | 
					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/markdown.dart';
 | 
					import 'package:island/widgets/content/markdown.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
				
			||||||
import 'package:relative_time/relative_time.dart';
 | 
					import 'package:relative_time/relative_time.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';
 | 
				
			||||||
@@ -62,6 +65,10 @@ class NotificationUnreadCountNotifier
 | 
				
			|||||||
    final current = await future;
 | 
					    final current = await future;
 | 
				
			||||||
    state = AsyncData(math.max(current - count, 0));
 | 
					    state = AsyncData(math.max(current - count, 0));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void clear() async {
 | 
				
			||||||
 | 
					    state = AsyncData(0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
@@ -111,8 +118,27 @@ class NotificationScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    Future<void> markAllRead() async {
 | 
				
			||||||
 | 
					      showLoadingModal(context);
 | 
				
			||||||
 | 
					      final apiClient = ref.watch(apiClientProvider);
 | 
				
			||||||
 | 
					      await apiClient.post('/pusher/notifications/all/read');
 | 
				
			||||||
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					      hideLoadingModal(context);
 | 
				
			||||||
 | 
					      ref.invalidate(notificationListNotifierProvider);
 | 
				
			||||||
 | 
					      ref.watch(notificationUnreadCountNotifierProvider.notifier).clear();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(title: const Text('notifications').tr()),
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        title: const Text('notifications').tr(),
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          IconButton(
 | 
				
			||||||
 | 
					            onPressed: markAllRead,
 | 
				
			||||||
 | 
					            icon: const Icon(Symbols.mark_as_unread),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          const Gap(8),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: PagingHelperView(
 | 
					      body: PagingHelperView(
 | 
				
			||||||
        provider: notificationListNotifierProvider,
 | 
					        provider: notificationListNotifierProvider,
 | 
				
			||||||
        futureRefreshable: notificationListNotifierProvider.future,
 | 
					        futureRefreshable: notificationListNotifierProvider.future,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,7 +430,9 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
          const Gap(8),
 | 
					          const Gap(8),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: SafeArea(
 | 
					      body: Column(
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
            child: Form(
 | 
					            child: Form(
 | 
				
			||||||
              key: ValueKey(model.id),
 | 
					              key: ValueKey(model.id),
 | 
				
			||||||
              child: ListView(
 | 
					              child: ListView(
 | 
				
			||||||
@@ -436,8 +440,8 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  TextFormField(
 | 
					                  TextFormField(
 | 
				
			||||||
                    initialValue: model.title ?? '',
 | 
					                    initialValue: model.title ?? '',
 | 
				
			||||||
                decoration: const InputDecoration(
 | 
					                    decoration: InputDecoration(
 | 
				
			||||||
                  labelText: 'Title',
 | 
					                      labelText: 'title'.tr(),
 | 
				
			||||||
                      border: OutlineInputBorder(
 | 
					                      border: OutlineInputBorder(
 | 
				
			||||||
                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
					                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
@@ -449,7 +453,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                        (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                        (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                    validator: (v) {
 | 
					                    validator: (v) {
 | 
				
			||||||
                      if (v == null || v.trim().isEmpty) {
 | 
					                      if (v == null || v.trim().isEmpty) {
 | 
				
			||||||
                    return 'Title is required';
 | 
					                        return 'pollTitleRequired'.tr();
 | 
				
			||||||
                      }
 | 
					                      }
 | 
				
			||||||
                      return null;
 | 
					                      return null;
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
@@ -457,8 +461,8 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                  const Gap(12),
 | 
					                  const Gap(12),
 | 
				
			||||||
                  TextFormField(
 | 
					                  TextFormField(
 | 
				
			||||||
                    initialValue: model.description ?? '',
 | 
					                    initialValue: model.description ?? '',
 | 
				
			||||||
                decoration: const InputDecoration(
 | 
					                    decoration: InputDecoration(
 | 
				
			||||||
                  labelText: 'Description',
 | 
					                      labelText: 'description'.tr(),
 | 
				
			||||||
                      alignLabelWithHint: true,
 | 
					                      alignLabelWithHint: true,
 | 
				
			||||||
                      border: OutlineInputBorder(
 | 
					                      border: OutlineInputBorder(
 | 
				
			||||||
                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
					                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
@@ -479,7 +483,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                  Row(
 | 
					                  Row(
 | 
				
			||||||
                    children: [
 | 
					                    children: [
 | 
				
			||||||
                      Text(
 | 
					                      Text(
 | 
				
			||||||
                    'Questions',
 | 
					                        'questions'.tr(),
 | 
				
			||||||
                        style: Theme.of(context).textTheme.titleLarge,
 | 
					                        style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      const Spacer(),
 | 
					                      const Spacer(),
 | 
				
			||||||
@@ -492,7 +496,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                                  : controller.open();
 | 
					                                  : controller.open();
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                            icon: const Icon(Icons.add),
 | 
					                            icon: const Icon(Icons.add),
 | 
				
			||||||
                        label: const Text('Add question'),
 | 
					                            label: Text('pollAddQuestion'.tr()),
 | 
				
			||||||
                          );
 | 
					                          );
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                        menuChildren:
 | 
					                        menuChildren:
 | 
				
			||||||
@@ -511,8 +515,9 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                  const Gap(8),
 | 
					                  const Gap(8),
 | 
				
			||||||
                  if (model.questions.isEmpty)
 | 
					                  if (model.questions.isEmpty)
 | 
				
			||||||
                    _EmptyState(
 | 
					                    _EmptyState(
 | 
				
			||||||
                  title: 'No questions yet',
 | 
					                      title: 'pollNoQuestionsYet'.tr(),
 | 
				
			||||||
                  subtitle: 'Use "Add question" to start building your poll.',
 | 
					                      subtitle:
 | 
				
			||||||
 | 
					                          'pollNoQuestionsHint'.tr(),
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                  else
 | 
					                  else
 | 
				
			||||||
                    ReorderableListView.builder(
 | 
					                    ReorderableListView.builder(
 | 
				
			||||||
@@ -559,7 +564,10 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                              const Divider(height: 1),
 | 
					                              const Divider(height: 1),
 | 
				
			||||||
                              Padding(
 | 
					                              Padding(
 | 
				
			||||||
                                padding: const EdgeInsets.all(16),
 | 
					                                padding: const EdgeInsets.all(16),
 | 
				
			||||||
                            child: _QuestionEditor(index: index, question: q),
 | 
					                                child: _QuestionEditor(
 | 
				
			||||||
 | 
					                                  index: index,
 | 
				
			||||||
 | 
					                                  question: q,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                            ],
 | 
					                            ],
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
@@ -571,21 +579,14 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
      bottomNavigationBar: Padding(
 | 
					          Row(
 | 
				
			||||||
        padding: EdgeInsets.fromLTRB(
 | 
					 | 
				
			||||||
          16,
 | 
					 | 
				
			||||||
          8,
 | 
					 | 
				
			||||||
          16,
 | 
					 | 
				
			||||||
          16 + MediaQuery.of(context).padding.bottom,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        child: Row(
 | 
					 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              OutlinedButton.icon(
 | 
					              OutlinedButton.icon(
 | 
				
			||||||
                onPressed: () {
 | 
					                onPressed: () {
 | 
				
			||||||
                  Navigator.of(context).maybePop();
 | 
					                  Navigator.of(context).maybePop();
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                icon: const Icon(Icons.close),
 | 
					                icon: const Icon(Icons.close),
 | 
				
			||||||
              label: const Text('Cancel'),
 | 
					                label: Text('cancel'.tr()),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              const Spacer(),
 | 
					              const Spacer(),
 | 
				
			||||||
              FilledButton.icon(
 | 
					              FilledButton.icon(
 | 
				
			||||||
@@ -593,10 +594,11 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
                  _submitPoll(context, ref);
 | 
					                  _submitPoll(context, ref);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                icon: const Icon(Icons.cloud_upload_outlined),
 | 
					                icon: const Icon(Icons.cloud_upload_outlined),
 | 
				
			||||||
              label: Text(model.id == null ? 'Create' : 'Update'),
 | 
					                label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -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),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,12 +51,12 @@ class PostSearchNotifier
 | 
				
			|||||||
      final offset = cursor == null ? 0 : int.parse(cursor);
 | 
					      final offset = cursor == null ? 0 : int.parse(cursor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      final response = await client.get(
 | 
					      final response = await client.get(
 | 
				
			||||||
        '/sphere/posts/search',
 | 
					        '/sphere/posts',
 | 
				
			||||||
        queryParameters: {
 | 
					        queryParameters: {
 | 
				
			||||||
          'query': _currentQuery,
 | 
					          'query': _currentQuery,
 | 
				
			||||||
          'offset': offset,
 | 
					          'offset': offset,
 | 
				
			||||||
          'take': _pageSize,
 | 
					          'take': _pageSize,
 | 
				
			||||||
          'useVector': false,
 | 
					          'vector': false,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
@@ -26,7 +26,12 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			|||||||
      final notification = SnNotification.fromJson(pkt.data!);
 | 
					      final notification = SnNotification.fromJson(pkt.data!);
 | 
				
			||||||
      showTopSnackBar(
 | 
					      showTopSnackBar(
 | 
				
			||||||
        globalOverlay.currentState!,
 | 
					        globalOverlay.currentState!,
 | 
				
			||||||
        NotificationCard(notification: notification),
 | 
					        Center(
 | 
				
			||||||
 | 
					          child: ConstrainedBox(
 | 
				
			||||||
 | 
					            constraints: const BoxConstraints(maxWidth: 480),
 | 
				
			||||||
 | 
					            child: NotificationCard(notification: notification),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          if (notification.meta['action_uri'] != null) {
 | 
					          if (notification.meta['action_uri'] != null) {
 | 
				
			||||||
            var uri = notification.meta['action_uri'] as String;
 | 
					            var uri = notification.meta['action_uri'] as String;
 | 
				
			||||||
@@ -53,9 +58,9 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			|||||||
                      (Platform.isMacOS ||
 | 
					                      (Platform.isMacOS ||
 | 
				
			||||||
                          Platform.isWindows ||
 | 
					                          Platform.isWindows ||
 | 
				
			||||||
                          Platform.isLinux))
 | 
					                          Platform.isLinux))
 | 
				
			||||||
                  ? 24
 | 
					                  ? 28
 | 
				
			||||||
                  // ignore: use_build_context_synchronously
 | 
					                  // ignore: use_build_context_synchronously
 | 
				
			||||||
                  : MediaQuery.of(context).padding.top + 8,
 | 
					                  : MediaQuery.of(context).padding.top + 16,
 | 
				
			||||||
          bottom: 16,
 | 
					          bottom: 16,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -67,6 +72,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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (context.mounted) {
 | 
				
			||||||
        await showUpdateSheet(context, release);
 | 
					        await showUpdateSheet(context, release);
 | 
				
			||||||
    } catch (_) {
 | 
					        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,8 +178,12 @@ class UpdateService {
 | 
				
			|||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      isScrollControlled: true,
 | 
					      isScrollControlled: true,
 | 
				
			||||||
      useRootNavigator: true,
 | 
					      useRootNavigator: true,
 | 
				
			||||||
      builder:
 | 
					      builder: (ctx) {
 | 
				
			||||||
          (ctx) => _UpdateSheet(
 | 
					        String? androidUpdateUrl;
 | 
				
			||||||
 | 
					        if (Platform.isAndroid) {
 | 
				
			||||||
 | 
					          androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return _UpdateSheet(
 | 
				
			||||||
          release: release,
 | 
					          release: release,
 | 
				
			||||||
          onOpen: () async {
 | 
					          onOpen: () async {
 | 
				
			||||||
            final uri = Uri.parse(release.htmlUrl);
 | 
					            final uri = Uri.parse(release.htmlUrl);
 | 
				
			||||||
@@ -135,16 +191,55 @@ class UpdateService {
 | 
				
			|||||||
              await launchUrl(uri, mode: LaunchMode.externalApplication);
 | 
					              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:
 | 
				
			||||||
 | 
					                      widget.release.body.isEmpty
 | 
				
			||||||
                          ? 'No changelog provided.'
 | 
					                          ? 'No changelog provided.'
 | 
				
			||||||
                      : release.body,
 | 
					                          : widget.release.body,
 | 
				
			||||||
                  style: theme.textTheme.bodyMedium,
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            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(
 | 
					                      Expanded(
 | 
				
			||||||
                        child: FilledButton.icon(
 | 
					                        child: FilledButton.icon(
 | 
				
			||||||
                        onPressed: onOpen,
 | 
					                          onPressed: () {
 | 
				
			||||||
 | 
					                            log(widget.androidUpdateUrl!);
 | 
				
			||||||
 | 
					                            _installUpdate(widget.androidUpdateUrl!);
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          icon: const Icon(Symbols.update),
 | 
				
			||||||
 | 
					                          label: const Text('Install update'),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    Expanded(
 | 
				
			||||||
 | 
					                      child: FilledButton.icon(
 | 
				
			||||||
 | 
					                        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'),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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',
 | 
					          '/id/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
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
@@ -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
 | 
				
			||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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'),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,12 @@ export 'content/alert.native.dart'
 | 
				
			|||||||
void showSnackBar(String message, {SnackBarAction? action}) {
 | 
					void showSnackBar(String message, {SnackBarAction? action}) {
 | 
				
			||||||
  showTopSnackBar(
 | 
					  showTopSnackBar(
 | 
				
			||||||
    globalOverlay.currentState!,
 | 
					    globalOverlay.currentState!,
 | 
				
			||||||
    Card(child: Text(message).padding(horizontal: 20, vertical: 16)),
 | 
					    ConstrainedBox(
 | 
				
			||||||
 | 
					      constraints: const BoxConstraints(maxWidth: 480),
 | 
				
			||||||
 | 
					      child: Center(
 | 
				
			||||||
 | 
					        child: Card(child: Text(message).padding(horizontal: 20, vertical: 16)),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    snackBarPosition: SnackBarPosition.bottom,
 | 
					    snackBarPosition: SnackBarPosition.bottom,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -69,7 +74,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()),
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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: () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,11 +57,11 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
				
			|||||||
                    Row(
 | 
					                    Row(
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        // Favicon
 | 
					                        // Favicon
 | 
				
			||||||
                        if (link.faviconUrl.isNotEmpty) ...[
 | 
					                        if (link.faviconUrl?.isNotEmpty ?? false) ...[
 | 
				
			||||||
                          ClipRRect(
 | 
					                          ClipRRect(
 | 
				
			||||||
                            borderRadius: BorderRadius.circular(4),
 | 
					                            borderRadius: BorderRadius.circular(4),
 | 
				
			||||||
                            child: UniversalImage(
 | 
					                            child: UniversalImage(
 | 
				
			||||||
                              uri: link.faviconUrl,
 | 
					                              uri: link.faviconUrl!,
 | 
				
			||||||
                              width: 16,
 | 
					                              width: 16,
 | 
				
			||||||
                              height: 16,
 | 
					                              height: 16,
 | 
				
			||||||
                              fit: BoxFit.cover,
 | 
					                              fit: BoxFit.cover,
 | 
				
			||||||
@@ -80,8 +80,8 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
				
			|||||||
                        // Site name
 | 
					                        // Site name
 | 
				
			||||||
                        Expanded(
 | 
					                        Expanded(
 | 
				
			||||||
                          child: Text(
 | 
					                          child: Text(
 | 
				
			||||||
                            link.siteName.isNotEmpty
 | 
					                            (link.siteName?.isNotEmpty ?? false)
 | 
				
			||||||
                                ? link.siteName
 | 
					                                ? link.siteName!
 | 
				
			||||||
                                : Uri.parse(link.url).host,
 | 
					                                : Uri.parse(link.url).host,
 | 
				
			||||||
                            style: theme.textTheme.bodySmall?.copyWith(
 | 
					                            style: theme.textTheme.bodySmall?.copyWith(
 | 
				
			||||||
                              color: colorScheme.onSurfaceVariant,
 | 
					                              color: colorScheme.onSurfaceVariant,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,16 +176,22 @@ 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,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              final content = ConstrainedBox(
 | 
					              final content = ClipRRect(
 | 
				
			||||||
 | 
					                borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                child: ConstrainedBox(
 | 
				
			||||||
                  constraints: BoxConstraints(maxHeight: 360),
 | 
					                  constraints: BoxConstraints(maxHeight: 360),
 | 
				
			||||||
                child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain),
 | 
					                  child: UniversalImage(
 | 
				
			||||||
 | 
					                    uri: uri.toString(),
 | 
				
			||||||
 | 
					                    fit: BoxFit.contain,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
              return content;
 | 
					              return content;
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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},
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,78 +57,93 @@ 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),
 | 
				
			||||||
 | 
					                PagingHelperSliverView(
 | 
				
			||||||
                  provider: pollFeedbackNotifierProvider(pollId),
 | 
					                  provider: pollFeedbackNotifierProvider(pollId),
 | 
				
			||||||
              futureRefreshable: pollFeedbackNotifierProvider(pollId).future,
 | 
					                  futureRefreshable:
 | 
				
			||||||
 | 
					                      pollFeedbackNotifierProvider(pollId).future,
 | 
				
			||||||
                  notifierRefreshable:
 | 
					                  notifierRefreshable:
 | 
				
			||||||
                      pollFeedbackNotifierProvider(pollId).notifier,
 | 
					                      pollFeedbackNotifierProvider(pollId).notifier,
 | 
				
			||||||
                  contentBuilder:
 | 
					                  contentBuilder:
 | 
				
			||||||
                  (data, widgetCount, endItemView) => ListView.separated(
 | 
					                      (val, widgetCount, endItemView) => SliverList.separated(
 | 
				
			||||||
                    padding: const EdgeInsets.symmetric(vertical: 4),
 | 
					 | 
				
			||||||
                        itemCount: widgetCount,
 | 
					                        itemCount: widgetCount,
 | 
				
			||||||
                        itemBuilder: (context, index) {
 | 
					                        itemBuilder: (context, index) {
 | 
				
			||||||
                          if (index == widgetCount - 1) {
 | 
					                          if (index == widgetCount - 1) {
 | 
				
			||||||
                            // Provided by PagingHelperView to indicate end/loading
 | 
					                            // Provided by PagingHelperView to indicate end/loading
 | 
				
			||||||
                            return endItemView;
 | 
					                            return endItemView;
 | 
				
			||||||
                          }
 | 
					                          }
 | 
				
			||||||
                      final answer = data.items[index];
 | 
					                          final answer = val.items[index];
 | 
				
			||||||
                      return _PollAnswerTile(answer: answer, poll: poll);
 | 
					                          return _PollAnswerTile(answer: answer, poll: data);
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                        separatorBuilder:
 | 
					                        separatorBuilder:
 | 
				
			||||||
                            (context, index) =>
 | 
					                            (context, index) =>
 | 
				
			||||||
                                const Divider(height: 1).padding(vertical: 4),
 | 
					                                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) {
 | 
				
			||||||
    final theme = Theme.of(context);
 | 
					    final theme = Theme.of(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Column(
 | 
					    return Column(
 | 
				
			||||||
 | 
					      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					      spacing: 12,
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        if (poll.title != null || (poll.description?.isNotEmpty ?? false))
 | 
				
			||||||
 | 
					          Column(
 | 
				
			||||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              if (poll.title != null)
 | 
					              if (poll.title != null)
 | 
				
			||||||
                Text(poll.title!, style: theme.textTheme.titleLarge),
 | 
					                Text(poll.title!, style: theme.textTheme.titleLarge),
 | 
				
			||||||
        if (poll.description != null)
 | 
					              if (poll.description?.isNotEmpty ?? false)
 | 
				
			||||||
          Padding(
 | 
					                Text(
 | 
				
			||||||
            padding: const EdgeInsets.only(top: 2),
 | 
					 | 
				
			||||||
            child: Text(
 | 
					 | 
				
			||||||
                  poll.description!,
 | 
					                  poll.description!,
 | 
				
			||||||
                  style: theme.textTheme.bodyMedium?.copyWith(
 | 
					                  style: theme.textTheme.bodyMedium?.copyWith(
 | 
				
			||||||
                    color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
					                    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(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										233
									
								
								lib/widgets/poll/poll_stats_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								lib/widgets/poll/poll_stats_widget.dart
									
									
									
									
									
										Normal 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),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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 ?? {});
 | 
				
			||||||
 | 
					    if (!widget.isReadonly) {
 | 
				
			||||||
      _loadCurrentIntoLocalState();
 | 
					      _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)),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					      if (!widget.isReadonly) {
 | 
				
			||||||
        _loadCurrentIntoLocalState();
 | 
					        _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});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -244,7 +244,6 @@ class ComposeSettingsSheet extends HookConsumerWidget {
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Categories field
 | 
					            // Categories field
 | 
				
			||||||
            // FIXME: Sometimes the entire dropdown crashes: 'package:flutter/src/rendering/stack.dart': Failed assertion: line 799 pos 12: 'firstChild == null || child != null': is not true.
 | 
					 | 
				
			||||||
            DropdownButtonFormField2<SnPostCategory>(
 | 
					            DropdownButtonFormField2<SnPostCategory>(
 | 
				
			||||||
              isExpanded: true,
 | 
					              isExpanded: true,
 | 
				
			||||||
              decoration: InputDecoration(
 | 
					              decoration: InputDecoration(
 | 
				
			||||||
@@ -306,7 +305,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
 | 
				
			|||||||
              value: currentCategories.isEmpty ? null : currentCategories.last,
 | 
					              value: currentCategories.isEmpty ? null : currentCategories.last,
 | 
				
			||||||
              onChanged: (_) {},
 | 
					              onChanged: (_) {},
 | 
				
			||||||
              selectedItemBuilder: (context) {
 | 
					              selectedItemBuilder: (context) {
 | 
				
			||||||
                return currentCategories.map((item) {
 | 
					                return (postCategories.value ?? []).map((item) {
 | 
				
			||||||
                  return SingleChildScrollView(
 | 
					                  return SingleChildScrollView(
 | 
				
			||||||
                    scrollDirection: Axis.horizontal,
 | 
					                    scrollDirection: Axis.horizontal,
 | 
				
			||||||
                    child: Row(
 | 
					                    child: Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
 | 
				
			|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_item.dart';
 | 
					import 'package:island/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/config.dart'; // Import config.dart for shared preferences keys and provider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'post_featured.g.dart';
 | 
					part 'post_featured.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,7 +26,13 @@ class PostFeaturedList extends HookConsumerWidget {
 | 
				
			|||||||
    final featuredPostsAsync = ref.watch(featuredPostsProvider);
 | 
					    final featuredPostsAsync = ref.watch(featuredPostsProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final pageViewController = usePageController();
 | 
					    final pageViewController = usePageController();
 | 
				
			||||||
 | 
					    final prefs = ref.watch(sharedPreferencesProvider);
 | 
				
			||||||
    final pageViewCurrent = useState(0);
 | 
					    final pageViewCurrent = useState(0);
 | 
				
			||||||
 | 
					    final previousFirstPostId = useState<String?>(null);
 | 
				
			||||||
 | 
					    final storedCollapsedId = useState<String?>(
 | 
				
			||||||
 | 
					      prefs.getString(kFeaturedPostsCollapsedId),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final isCollapsed = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      pageViewController.addListener(() {
 | 
					      pageViewController.addListener(() {
 | 
				
			||||||
@@ -34,6 +41,59 @@ class PostFeaturedList extends HookConsumerWidget {
 | 
				
			|||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }, [pageViewController]);
 | 
					    }, [pageViewController]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Log isCollapsed state changes
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      debugPrint(
 | 
				
			||||||
 | 
					        'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }, [isCollapsed.value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) {
 | 
				
			||||||
 | 
					        final currentFirstPostId = featuredPostsAsync.value!.first.id;
 | 
				
			||||||
 | 
					        debugPrint(
 | 
				
			||||||
 | 
					          'PostFeaturedList: Current first post ID: $currentFirstPostId',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        debugPrint(
 | 
				
			||||||
 | 
					          'PostFeaturedList: Previous first post ID: ${previousFirstPostId.value}',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        debugPrint(
 | 
				
			||||||
 | 
					          'PostFeaturedList: Stored collapsed ID: ${storedCollapsedId.value}',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (previousFirstPostId.value == null) {
 | 
				
			||||||
 | 
					          // Initial load
 | 
				
			||||||
 | 
					          previousFirstPostId.value = currentFirstPostId;
 | 
				
			||||||
 | 
					          isCollapsed.value = (storedCollapsedId.value == currentFirstPostId);
 | 
				
			||||||
 | 
					          debugPrint(
 | 
				
			||||||
 | 
					            'PostFeaturedList: Initial load. isCollapsed set to ${isCollapsed.value}',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } else if (previousFirstPostId.value != currentFirstPostId) {
 | 
				
			||||||
 | 
					          // First post changed, expand by default
 | 
				
			||||||
 | 
					          previousFirstPostId.value = currentFirstPostId;
 | 
				
			||||||
 | 
					          isCollapsed.value = false;
 | 
				
			||||||
 | 
					          prefs.remove(
 | 
				
			||||||
 | 
					            kFeaturedPostsCollapsedId,
 | 
				
			||||||
 | 
					          ); // Clear stored ID if post changes
 | 
				
			||||||
 | 
					          debugPrint(
 | 
				
			||||||
 | 
					            'PostFeaturedList: First post changed. isCollapsed set to false.',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Same first post, maintain current collapse state
 | 
				
			||||||
 | 
					          // No change needed for isCollapsed.value unless manually toggled
 | 
				
			||||||
 | 
					          debugPrint(
 | 
				
			||||||
 | 
					            'PostFeaturedList: Same first post. Maintaining current collapse state.',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        debugPrint(
 | 
				
			||||||
 | 
					          'PostFeaturedList: featuredPostsAsync has no value or is empty.',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }, [featuredPostsAsync.value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ClipRRect(
 | 
					    return ClipRRect(
 | 
				
			||||||
      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
      child: Card(
 | 
					      child: Card(
 | 
				
			||||||
@@ -73,10 +133,48 @@ class PostFeaturedList extends HookConsumerWidget {
 | 
				
			|||||||
                  },
 | 
					                  },
 | 
				
			||||||
                  icon: const Icon(Symbols.arrow_right),
 | 
					                  icon: const Icon(Symbols.arrow_right),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					                IconButton(
 | 
				
			||||||
 | 
					                  padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					                  visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					                  constraints: const BoxConstraints(),
 | 
				
			||||||
 | 
					                  onPressed: () {
 | 
				
			||||||
 | 
					                    isCollapsed.value = !isCollapsed.value;
 | 
				
			||||||
 | 
					                    debugPrint(
 | 
				
			||||||
 | 
					                      'PostFeaturedList: Manual toggle. isCollapsed set to ${isCollapsed.value}',
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    if (isCollapsed.value &&
 | 
				
			||||||
 | 
					                        featuredPostsAsync.hasValue &&
 | 
				
			||||||
 | 
					                        featuredPostsAsync.value!.isNotEmpty) {
 | 
				
			||||||
 | 
					                      prefs.setString(
 | 
				
			||||||
 | 
					                        kFeaturedPostsCollapsedId,
 | 
				
			||||||
 | 
					                        featuredPostsAsync.value!.first.id,
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                      debugPrint(
 | 
				
			||||||
 | 
					                        'PostFeaturedList: Stored collapsed ID: ${featuredPostsAsync.value!.first.id}',
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                      prefs.remove(kFeaturedPostsCollapsedId);
 | 
				
			||||||
 | 
					                      debugPrint(
 | 
				
			||||||
 | 
					                        'PostFeaturedList: Removed stored collapsed ID.',
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                  icon: Icon(
 | 
				
			||||||
 | 
					                    isCollapsed.value
 | 
				
			||||||
 | 
					                        ? Symbols.expand_more
 | 
				
			||||||
 | 
					                        : Symbols.expand_less,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ).padding(horizontal: 16, vertical: 8),
 | 
					            ).padding(horizontal: 16, vertical: 8),
 | 
				
			||||||
            featuredPostsAsync.when(
 | 
					            AnimatedSize(
 | 
				
			||||||
              loading: () => const Center(child: CircularProgressIndicator()),
 | 
					              duration: const Duration(milliseconds: 300),
 | 
				
			||||||
 | 
					              curve: Curves.easeInOut,
 | 
				
			||||||
 | 
					              child: Visibility(
 | 
				
			||||||
 | 
					                visible: !isCollapsed.value,
 | 
				
			||||||
 | 
					                child: featuredPostsAsync.when(
 | 
				
			||||||
 | 
					                  loading:
 | 
				
			||||||
 | 
					                      () => const Center(child: CircularProgressIndicator()),
 | 
				
			||||||
                  error: (error, stack) => Center(child: Text('Error: $error')),
 | 
					                  error: (error, stack) => Center(child: Text('Error: $error')),
 | 
				
			||||||
                  data: (posts) {
 | 
					                  data: (posts) {
 | 
				
			||||||
                    return SizedBox(
 | 
					                    return SizedBox(
 | 
				
			||||||
@@ -97,6 +195,8 @@ class PostFeaturedList extends HookConsumerWidget {
 | 
				
			|||||||
                    );
 | 
					                    );
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										135
									
								
								lib/widgets/post/post_item_screenshot.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								lib/widgets/post/post_item_screenshot.dart
									
									
									
									
									
										Normal 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),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										841
									
								
								lib/widgets/post/post_shared.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										841
									
								
								lib/widgets/post/post_shared.dart
									
									
									
									
									
										Normal 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']}'),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              )),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user