Compare commits
	
		
			46 Commits
		
	
	
		
			3.1.0+120
			...
			85ff52a661
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
							
								
								
									
										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 | ||||||
|  |           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") | ||||||
|   | |||||||
| @@ -573,6 +573,7 @@ | |||||||
|   "keyboardShortcuts": "Keyboard Shortcuts", |   "keyboardShortcuts": "Keyboard Shortcuts", | ||||||
|   "share": "Share", |   "share": "Share", | ||||||
|   "sharePost": "Share Post", |   "sharePost": "Share Post", | ||||||
|  |   "sharePostPhoto": "Share Post as Photo", | ||||||
|   "quickActions": "Quick Actions", |   "quickActions": "Quick Actions", | ||||||
|   "post": "Post", |   "post": "Post", | ||||||
|   "copy": "Copy", |   "copy": "Copy", | ||||||
| @@ -706,6 +707,7 @@ | |||||||
|   "copyToClipboardTooltip": "Copy to clipboard", |   "copyToClipboardTooltip": "Copy to clipboard", | ||||||
|   "postForwardingTo": "Forwarding to", |   "postForwardingTo": "Forwarding to", | ||||||
|   "postReplyingTo": "Replying to", |   "postReplyingTo": "Replying to", | ||||||
|  |   "postReplyPlaceholder": "Post your reply", | ||||||
|   "postEditing": "You are editing an existing post", |   "postEditing": "You are editing an existing post", | ||||||
|   "postArticle": "Article", |   "postArticle": "Article", | ||||||
|   "aboutDeviceName": "Device Name", |   "aboutDeviceName": "Device Name", | ||||||
| @@ -759,6 +761,7 @@ | |||||||
|   "pollsRecent": "Recent Polls", |   "pollsRecent": "Recent Polls", | ||||||
|   "pollCreateNew": "Create New", |   "pollCreateNew": "Create New", | ||||||
|   "pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.", |   "pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.", | ||||||
|  |   "pollQuestions": "Questions", | ||||||
|   "publisher": "Publisher", |   "publisher": "Publisher", | ||||||
|   "publisherHint": "Enter the publisher name", |   "publisherHint": "Enter the publisher name", | ||||||
|   "publisherCannotBeEmpty": "Publisher cannot be empty", |   "publisherCannotBeEmpty": "Publisher cannot be empty", | ||||||
| @@ -787,5 +790,51 @@ | |||||||
|   "addLink": "Add link", |   "addLink": "Add link", | ||||||
|   "linkKey": "Link Name", |   "linkKey": "Link Name", | ||||||
|   "linkValue": "URL", |   "linkValue": "URL", | ||||||
|   "debugOptions": "Debug Options" |   "debugOptions": "Debug Options", | ||||||
|  |   "joinedAt": "Joined at {}", | ||||||
|  |   "searchAccounts": "Search accounts...", | ||||||
|  |   "webFeeds": "Web Feeds", | ||||||
|  |   "polls": "Polls", | ||||||
|  |   "sharePostSlogan": "Explore more on the Solar Network", | ||||||
|  |   "filesListAdditional": { | ||||||
|  |     "one": "+{} file remaining", | ||||||
|  |     "other": "+{} files remaining" | ||||||
|  |   }, | ||||||
|  |   "pollAnswerSubmitted": "Poll answer has been submitted.", | ||||||
|  |   "modifyAnswers": "Modify Answers", | ||||||
|  |   "back": "Back", | ||||||
|  |   "submit": "Submit", | ||||||
|  |   "pollOptionDefaultLabel": "Option 1", | ||||||
|  |   "pollUpdated": "Poll updated.", | ||||||
|  |   "pollCreated": "Poll created.", | ||||||
|  |   "pollCreate": "Create Poll", | ||||||
|  |   "pollEdit": "Edit Poll", | ||||||
|  |   "pollPreviewJsonDebug": "Debug Preview", | ||||||
|  |   "pollTitleRequired": "Title is required", | ||||||
|  |   "pollEndDateOptional": "End date & time (optional)", | ||||||
|  |   "notSet": "Not set", | ||||||
|  |   "pick": "Pick", | ||||||
|  |   "clear": "Clear", | ||||||
|  |   "questions": "Questions", | ||||||
|  |   "pollAddQuestion": "Add question", | ||||||
|  |   "pollQuestionTypeSingleChoice": "Single choice", | ||||||
|  |   "pollQuestionTypeMultipleChoice": "Multiple choice", | ||||||
|  |   "pollQuestionTypeFreeText": "Free text", | ||||||
|  |   "pollQuestionTypeYesNo": "Yes / No", | ||||||
|  |   "pollQuestionTypeRating": "Rating", | ||||||
|  |   "pollNoQuestionsYet": "No questions yet", | ||||||
|  |   "pollNoQuestionsHint": "Use \"Add question\" to start building your poll.", | ||||||
|  |   "pollDebugPreview": "Debug Preview", | ||||||
|  |   "pollUntitledQuestion": "Untitled question", | ||||||
|  |   "moveUp": "Move up", | ||||||
|  |   "moveDown": "Move down", | ||||||
|  |   "required": "Required", | ||||||
|  |   "pollQuestionTitle": "Question title", | ||||||
|  |   "pollQuestionTitleRequired": "Question title is required", | ||||||
|  |   "pollQuestionDescriptionOptional": "Question description (optional)", | ||||||
|  |   "options": "Options", | ||||||
|  |   "pollAddOption": "Add option", | ||||||
|  |   "pollOptionLabel": "Option label", | ||||||
|  |   "pollLongTextAnswerPreview": "Long text answer (preview)", | ||||||
|  |   "pollShortTextAnswerPreview": "Short text answer (preview)" | ||||||
| } | } | ||||||
| @@ -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": "反应", | ||||||
| @@ -350,11 +344,10 @@ | |||||||
|     "accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。", |     "accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。", | ||||||
|     "unauthorized": "未授权", |     "unauthorized": "未授权", | ||||||
|     "unauthorizedHint": "您未登录或会话已过期,请重新登录。", |     "unauthorizedHint": "您未登录或会话已过期,请重新登录。", | ||||||
|   "publisherBelongsTo": "属于 {}", |     "publisherBelongsTo": "属于", | ||||||
|     "postContent": "内容", |     "postContent": "内容", | ||||||
|     "postSettings": "设置", |     "postSettings": "设置", | ||||||
|     "postPublisherUnselected": "未指定发布者", |     "postPublisherUnselected": "未指定发布者", | ||||||
|   "postVisibility": "可见性", |  | ||||||
|     "postVisibilityPublic": "公开", |     "postVisibilityPublic": "公开", | ||||||
|     "postVisibilityFriends": "仅好友可见", |     "postVisibilityFriends": "仅好友可见", | ||||||
|     "postVisibilityUnlisted": "不公开", |     "postVisibilityUnlisted": "不公开", | ||||||
| @@ -495,20 +488,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 +517,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 +706,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 | 
							
								
								
									
										135
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								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,6 +241,8 @@ PODS: | |||||||
|   - pointer_interceptor_ios (0.0.1): |   - pointer_interceptor_ios (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - PromisesObjC (2.4.0) |   - PromisesObjC (2.4.0) | ||||||
|  |   - PromisesSwift (2.4.0): | ||||||
|  |     - PromisesObjC (= 2.4.0) | ||||||
|   - receive_sharing_intent (1.8.1): |   - receive_sharing_intent (1.8.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - record_ios (1.0.0): |   - record_ios (1.0.0): | ||||||
| @@ -178,25 +261,25 @@ PODS: | |||||||
|   - sqflite_darwin (0.0.4): |   - sqflite_darwin (0.0.4): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - sqlite3 (3.50.3): |   - sqlite3 (3.50.4): | ||||||
|     - sqlite3/common (= 3.50.3) |     - sqlite3/common (= 3.50.4) | ||||||
|   - sqlite3/common (3.50.3) |   - sqlite3/common (3.50.4) | ||||||
|   - sqlite3/dbstatvtab (3.50.3): |   - sqlite3/dbstatvtab (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/fts5 (3.50.3): |   - sqlite3/fts5 (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/math (3.50.3): |   - sqlite3/math (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/perf-threadsafe (3.50.3): |   - sqlite3/perf-threadsafe (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/rtree (3.50.3): |   - sqlite3/rtree (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/session (3.50.3): |   - sqlite3/session (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3_flutter_libs (0.0.1): |   - sqlite3_flutter_libs (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|     - sqlite3 (~> 3.50.3) |     - sqlite3 (~> 3.50.4) | ||||||
|     - sqlite3/dbstatvtab |     - sqlite3/dbstatvtab | ||||||
|     - sqlite3/fts5 |     - sqlite3/fts5 | ||||||
|     - sqlite3/math |     - sqlite3/math | ||||||
| @@ -220,9 +303,12 @@ DEPENDENCIES: | |||||||
|   - croppy (from `.symlinks/plugins/croppy/ios`) |   - croppy (from `.symlinks/plugins/croppy/ios`) | ||||||
|   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) |   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) | ||||||
|   - file_picker (from `.symlinks/plugins/file_picker/ios`) |   - file_picker (from `.symlinks/plugins/file_picker/ios`) | ||||||
|  |   - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) | ||||||
|   - firebase_core (from `.symlinks/plugins/firebase_core/ios`) |   - firebase_core (from `.symlinks/plugins/firebase_core/ios`) | ||||||
|  |   - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) | ||||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) |   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||||
|   - Flutter (from `Flutter`) |   - Flutter (from `Flutter`) | ||||||
|  |   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) |   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) | ||||||
|   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) |   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) | ||||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) |   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||||
| @@ -262,16 +348,24 @@ SPEC REPOS: | |||||||
|     - DKImagePickerController |     - DKImagePickerController | ||||||
|     - DKPhotoGallery |     - DKPhotoGallery | ||||||
|     - Firebase |     - Firebase | ||||||
|  |     - FirebaseAnalytics | ||||||
|     - FirebaseCore |     - FirebaseCore | ||||||
|  |     - FirebaseCoreExtension | ||||||
|     - FirebaseCoreInternal |     - FirebaseCoreInternal | ||||||
|  |     - FirebaseCrashlytics | ||||||
|     - FirebaseInstallations |     - FirebaseInstallations | ||||||
|     - FirebaseMessaging |     - FirebaseMessaging | ||||||
|  |     - FirebaseRemoteConfigInterop | ||||||
|  |     - FirebaseSessions | ||||||
|  |     - GoogleAdsOnDeviceConversion | ||||||
|  |     - GoogleAppMeasurement | ||||||
|     - GoogleDataTransport |     - GoogleDataTransport | ||||||
|     - GoogleUtilities |     - GoogleUtilities | ||||||
|     - Kingfisher |     - Kingfisher | ||||||
|     - nanopb |     - nanopb | ||||||
|     - OrderedSet |     - OrderedSet | ||||||
|     - PromisesObjC |     - PromisesObjC | ||||||
|  |     - PromisesSwift | ||||||
|     - SAMKeychain |     - SAMKeychain | ||||||
|     - SDWebImage |     - SDWebImage | ||||||
|     - sqlite3 |     - sqlite3 | ||||||
| @@ -287,12 +381,18 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/device_info_plus/ios" |     :path: ".symlinks/plugins/device_info_plus/ios" | ||||||
|   file_picker: |   file_picker: | ||||||
|     :path: ".symlinks/plugins/file_picker/ios" |     :path: ".symlinks/plugins/file_picker/ios" | ||||||
|  |   firebase_analytics: | ||||||
|  |     :path: ".symlinks/plugins/firebase_analytics/ios" | ||||||
|   firebase_core: |   firebase_core: | ||||||
|     :path: ".symlinks/plugins/firebase_core/ios" |     :path: ".symlinks/plugins/firebase_core/ios" | ||||||
|  |   firebase_crashlytics: | ||||||
|  |     :path: ".symlinks/plugins/firebase_crashlytics/ios" | ||||||
|   firebase_messaging: |   firebase_messaging: | ||||||
|     :path: ".symlinks/plugins/firebase_messaging/ios" |     :path: ".symlinks/plugins/firebase_messaging/ios" | ||||||
|   Flutter: |   Flutter: | ||||||
|     :path: Flutter |     :path: Flutter | ||||||
|  |   flutter_app_update: | ||||||
|  |     :path: ".symlinks/plugins/flutter_app_update/ios" | ||||||
|   flutter_inappwebview_ios: |   flutter_inappwebview_ios: | ||||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" |     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||||
|   flutter_keyboard_visibility: |   flutter_keyboard_visibility: | ||||||
| @@ -365,13 +465,21 @@ SPEC CHECKSUMS: | |||||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 |   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be |   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||||
|   Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 |   Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 | ||||||
|  |   firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d | ||||||
|   firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 |   firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 | ||||||
|  |   firebase_crashlytics: 2c6c1a17900a38081d938330e9f48e60ec5b255d | ||||||
|   firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361 |   firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361 | ||||||
|  |   FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7 | ||||||
|   FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a |   FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a | ||||||
|  |   FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361 | ||||||
|   FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 |   FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 | ||||||
|  |   FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7 | ||||||
|   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 |   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 | ||||||
|   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde |   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde | ||||||
|  |   FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613 | ||||||
|  |   FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42 | ||||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 |   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||||
|  |   flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9 | ||||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 |   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||||
|   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 |   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 | ||||||
|   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf |   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf | ||||||
| @@ -381,6 +489,8 @@ SPEC CHECKSUMS: | |||||||
|   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 |   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 | ||||||
|   flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457 |   flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457 | ||||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 |   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||||
|  |   GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 | ||||||
|  |   GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3 | ||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 |   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a |   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||||
| @@ -398,6 +508,7 @@ SPEC CHECKSUMS: | |||||||
|   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 |   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||||
|   pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed |   pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed | ||||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 |   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||||
|  |   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 | ||||||
|   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 |   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 | ||||||
|   record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b |   record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b | ||||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c |   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||||
| @@ -406,8 +517,8 @@ SPEC CHECKSUMS: | |||||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 |   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||||
|   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 |   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 | ||||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 |   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||||
|   sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 |   sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b | ||||||
|   sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e |   sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 | ||||||
|   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 |   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 | ||||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 |   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||||
|   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d |   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d | ||||||
|   | |||||||
| @@ -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,16 @@ 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 | ||||||
|  |       if (kIsWeb || !Platform.isWindows) { | ||||||
|  |         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 +154,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 +182,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 { | ||||||
|   | |||||||
| @@ -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>) | ||||||
|   | |||||||
| @@ -25,6 +25,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 +64,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, | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| 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/user.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:firebase_analytics/firebase_analytics.dart'; | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| @@ -59,6 +61,9 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|   return GoRouter( |   return GoRouter( | ||||||
|     navigatorKey: rootNavigatorKey, |     navigatorKey: rootNavigatorKey, | ||||||
|     initialLocation: '/', |     initialLocation: '/', | ||||||
|  |     observers: [ | ||||||
|  |       FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), | ||||||
|  |     ], | ||||||
|     routes: [ |     routes: [ | ||||||
|       ShellRoute( |       ShellRoute( | ||||||
|         navigatorKey: _shellNavigatorKey, |         navigatorKey: _shellNavigatorKey, | ||||||
|   | |||||||
| @@ -7,12 +7,12 @@ import 'package:flutter/services.dart'; | |||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/services/udid.native.dart'; | import 'package:island/services/udid.native.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; | import 'package:package_info_plus/package_info_plus.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:island/services/update_service.dart'; | import 'package:island/services/update_service.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; |  | ||||||
| import 'package:url_launcher/url_launcher.dart'; | import 'package:url_launcher/url_launcher.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
| @@ -205,33 +205,16 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | |||||||
|                                 // Fetch latest release and show the unified sheet |                                 // Fetch latest release and show the unified sheet | ||||||
|                                 final svc = UpdateService(); |                                 final svc = UpdateService(); | ||||||
|                                 // Reuse service fetch + compare to decide content |                                 // Reuse service fetch + compare to decide content | ||||||
|  |                                 showLoadingModal(context); | ||||||
|                                 final release = await svc.fetchLatestRelease(); |                                 final release = await svc.fetchLatestRelease(); | ||||||
|  |                                 if (!context.mounted) return; | ||||||
|  |                                 hideLoadingModal(context); | ||||||
|                                 if (release != null) { |                                 if (release != null) { | ||||||
|                                   await svc.showUpdateSheet(context, release); |                                   await svc.showUpdateSheet(context, release); | ||||||
|                                 } else { |                                 } else { | ||||||
|                                   // Fallback: show a simple sheet indicating no info |                                   showInfoAlert( | ||||||
|                                   // Use your SheetScaffold for consistent styling |                                     'Currently cannot get update from the GitHub.', | ||||||
|                                   // Show a minimal message |                                     'Unable to check for updates', | ||||||
|                                   // ignore: use_build_context_synchronously |  | ||||||
|                                   showModalBottomSheet( |  | ||||||
|                                     context: context, |  | ||||||
|                                     isScrollControlled: true, |  | ||||||
|                                     useSafeArea: true, |  | ||||||
|                                     showDragHandle: true, |  | ||||||
|                                     backgroundColor: |  | ||||||
|                                         Theme.of(context).colorScheme.surface, |  | ||||||
|                                     builder: |  | ||||||
|                                         (_) => const SheetScaffold( |  | ||||||
|                                           titleText: 'Update', |  | ||||||
|                                           child: Center( |  | ||||||
|                                             child: Padding( |  | ||||||
|                                               padding: EdgeInsets.all(24), |  | ||||||
|                                               child: Text( |  | ||||||
|                                                 'Unable to fetch release info at this time.', |  | ||||||
|                                               ), |  | ||||||
|                                             ), |  | ||||||
|                                           ), |  | ||||||
|                                         ), |  | ||||||
|                                   ); |                                   ); | ||||||
|                                 } |                                 } | ||||||
|                               }, |                               }, | ||||||
|   | |||||||
| @@ -236,7 +236,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), | ||||||
|   | |||||||
| @@ -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', | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ); |             ); | ||||||
|   | |||||||
| @@ -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/user.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,6 +1,8 @@ | |||||||
| 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'; | ||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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')), | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -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, | ||||||
|                         ), |                         ), | ||||||
|                   ), |                   ), | ||||||
|   | |||||||
| @@ -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(), | ||||||
|       ); |       ); | ||||||
|   | |||||||
| @@ -67,6 +67,9 @@ Future<void> subscribePushNotification( | |||||||
|   Dio apiClient, { |   Dio apiClient, { | ||||||
|   bool detailedErrors = false, |   bool detailedErrors = false, | ||||||
| }) async { | }) async { | ||||||
|  |   if (Platform.isLinux){ | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|   await FirebaseMessaging.instance.requestPermission( |   await FirebaseMessaging.instance.requestPermission( | ||||||
|     alert: true, |     alert: true, | ||||||
|     badge: true, |     badge: true, | ||||||
|   | |||||||
| @@ -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'), | ||||||
|                       ), |                       ), | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| 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'; | ||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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'), | ||||||
|   | |||||||
| @@ -69,7 +69,7 @@ void showLoadingModal(BuildContext context) { | |||||||
|                 child: Column( |                 child: Column( | ||||||
|                   mainAxisSize: MainAxisSize.min, |                   mainAxisSize: MainAxisSize.min, | ||||||
|                   children: [ |                   children: [ | ||||||
|                     CircularProgressIndicator(year2023: true), |                     CircularProgressIndicator(year2023: false), | ||||||
|                     const Gap(24), |                     const Gap(24), | ||||||
|                     Text('loading'.tr()), |                     Text('loading'.tr()), | ||||||
|                   ], |                   ], | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; | |||||||
| import 'package:flutter_highlight/themes/a11y-dark.dart'; | import 'package:flutter_highlight/themes/a11y-dark.dart'; | ||||||
| import 'package:flutter_highlight/themes/a11y-light.dart'; | import 'package:flutter_highlight/themes/a11y-light.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:google_fonts/google_fonts.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| @@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|             textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!, |             textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!, | ||||||
|           ), |           ), | ||||||
|           HrConfig(height: 1, color: Theme.of(context).dividerColor), |           HrConfig(height: 1, color: Theme.of(context).dividerColor), | ||||||
|           PreConfig(theme: isDark ? a11yDarkTheme : a11yLightTheme), |           PreConfig( | ||||||
|  |             theme: isDark ? a11yDarkTheme : a11yLightTheme, | ||||||
|  |             textStyle: GoogleFonts.robotoMono(fontSize: 14), | ||||||
|  |             styleNotMatched: GoogleFonts.robotoMono(fontSize: 14), | ||||||
|  |             decoration: BoxDecoration( | ||||||
|  |               color: Theme.of(context).colorScheme.surfaceContainerHighest, | ||||||
|  |               borderRadius: BorderRadius.all(Radius.circular(8.0)), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           TableConfig( | ||||||
|  |             wrapper: | ||||||
|  |                 (child) => SingleChildScrollView( | ||||||
|  |                   scrollDirection: Axis.horizontal, | ||||||
|  |                   child: child, | ||||||
|  |                 ), | ||||||
|  |           ), | ||||||
|           LinkConfig( |           LinkConfig( | ||||||
|             style: |             style: | ||||||
|                 linkStyle ?? |                 linkStyle ?? | ||||||
| @@ -160,7 +176,7 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|                           uri: stickerUri, |                           uri: stickerUri, | ||||||
|                           width: size, |                           width: size, | ||||||
|                           height: size, |                           height: size, | ||||||
|                           fit: BoxFit.cover, |                           fit: BoxFit.contain, | ||||||
|                           noCacheOptimization: true, |                           noCacheOptimization: true, | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
										
											
												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']}'), | ||||||
|  |                 }, | ||||||
|  |               )), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'post_item.dart'; | part of 'post_shared.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| @@ -10,7 +10,9 @@ import connectivity_plus | |||||||
| import device_info_plus | import device_info_plus | ||||||
| import file_picker | import file_picker | ||||||
| import file_selector_macos | import file_selector_macos | ||||||
|  | import firebase_analytics | ||||||
| import firebase_core | import firebase_core | ||||||
|  | import firebase_crashlytics | ||||||
| import firebase_messaging | import firebase_messaging | ||||||
| import flutter_inappwebview_macos | import flutter_inappwebview_macos | ||||||
| import flutter_platform_alert | import flutter_platform_alert | ||||||
| @@ -44,7 +46,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | |||||||
|   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) |   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) | ||||||
|   FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) |   FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) | ||||||
|   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) |   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) | ||||||
|  |   FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin")) | ||||||
|   FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) |   FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) | ||||||
|  |   FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) | ||||||
|   FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) |   FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) | ||||||
|   InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) |   InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) | ||||||
|   FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin")) |   FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin")) | ||||||
|   | |||||||
| @@ -13,23 +13,64 @@ PODS: | |||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - Firebase/CoreOnly (12.0.0): |   - Firebase/CoreOnly (12.0.0): | ||||||
|     - FirebaseCore (~> 12.0.0) |     - FirebaseCore (~> 12.0.0) | ||||||
|  |   - Firebase/Crashlytics (12.0.0): | ||||||
|  |     - Firebase/CoreOnly | ||||||
|  |     - FirebaseCrashlytics (~> 12.0.0) | ||||||
|   - Firebase/Messaging (12.0.0): |   - Firebase/Messaging (12.0.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseMessaging (~> 12.0.0) |     - FirebaseMessaging (~> 12.0.0) | ||||||
|  |   - firebase_analytics (12.0.0): | ||||||
|  |     - firebase_core | ||||||
|  |     - FirebaseAnalytics (= 12.0.0) | ||||||
|  |     - FlutterMacOS | ||||||
|   - firebase_core (4.0.0): |   - firebase_core (4.0.0): | ||||||
|     - Firebase/CoreOnly (~> 12.0.0) |     - Firebase/CoreOnly (~> 12.0.0) | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|  |   - firebase_crashlytics (5.0.0): | ||||||
|  |     - Firebase/CoreOnly (~> 12.0.0) | ||||||
|  |     - Firebase/Crashlytics (~> 12.0.0) | ||||||
|  |     - firebase_core | ||||||
|  |     - FlutterMacOS | ||||||
|   - firebase_messaging (16.0.0): |   - firebase_messaging (16.0.0): | ||||||
|     - Firebase/CoreOnly (~> 12.0.0) |     - Firebase/CoreOnly (~> 12.0.0) | ||||||
|     - Firebase/Messaging (~> 12.0.0) |     - Firebase/Messaging (~> 12.0.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|  |   - FirebaseAnalytics (12.0.0): | ||||||
|  |     - FirebaseAnalytics/Default (= 12.0.0) | ||||||
|  |     - FirebaseCore (~> 12.0.0) | ||||||
|  |     - FirebaseInstallations (~> 12.0.0) | ||||||
|  |     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/Network (~> 8.1) | ||||||
|  |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|  |     - nanopb (~> 3.30910.0) | ||||||
|  |   - FirebaseAnalytics/Default (12.0.0): | ||||||
|  |     - FirebaseCore (~> 12.0.0) | ||||||
|  |     - FirebaseInstallations (~> 12.0.0) | ||||||
|  |     - GoogleAppMeasurement/Default (= 12.0.0) | ||||||
|  |     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/Network (~> 8.1) | ||||||
|  |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|  |     - nanopb (~> 3.30910.0) | ||||||
|   - FirebaseCore (12.0.0): |   - FirebaseCore (12.0.0): | ||||||
|     - FirebaseCoreInternal (~> 12.0.0) |     - FirebaseCoreInternal (~> 12.0.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.1) |     - GoogleUtilities/Environment (~> 8.1) | ||||||
|     - GoogleUtilities/Logger (~> 8.1) |     - GoogleUtilities/Logger (~> 8.1) | ||||||
|  |   - FirebaseCoreExtension (12.0.0): | ||||||
|  |     - FirebaseCore (~> 12.0.0) | ||||||
|   - FirebaseCoreInternal (12.0.0): |   - FirebaseCoreInternal (12.0.0): | ||||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|  |   - FirebaseCrashlytics (12.0.0): | ||||||
|  |     - FirebaseCore (~> 12.0.0) | ||||||
|  |     - FirebaseInstallations (~> 12.0.0) | ||||||
|  |     - FirebaseRemoteConfigInterop (~> 12.0.0) | ||||||
|  |     - FirebaseSessions (~> 12.0.0) | ||||||
|  |     - GoogleDataTransport (~> 10.1) | ||||||
|  |     - GoogleUtilities/Environment (~> 8.1) | ||||||
|  |     - nanopb (~> 3.30910.0) | ||||||
|  |     - PromisesObjC (~> 2.4) | ||||||
|   - FirebaseInstallations (12.0.0): |   - FirebaseInstallations (12.0.0): | ||||||
|     - FirebaseCore (~> 12.0.0) |     - FirebaseCore (~> 12.0.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.1) |     - GoogleUtilities/Environment (~> 8.1) | ||||||
| @@ -44,6 +85,16 @@ PODS: | |||||||
|     - GoogleUtilities/Reachability (~> 8.1) |     - GoogleUtilities/Reachability (~> 8.1) | ||||||
|     - GoogleUtilities/UserDefaults (~> 8.1) |     - GoogleUtilities/UserDefaults (~> 8.1) | ||||||
|     - nanopb (~> 3.30910.0) |     - nanopb (~> 3.30910.0) | ||||||
|  |   - FirebaseRemoteConfigInterop (12.0.0) | ||||||
|  |   - FirebaseSessions (12.0.0): | ||||||
|  |     - FirebaseCore (~> 12.0.0) | ||||||
|  |     - FirebaseCoreExtension (~> 12.0.0) | ||||||
|  |     - FirebaseInstallations (~> 12.0.0) | ||||||
|  |     - GoogleDataTransport (~> 10.1) | ||||||
|  |     - GoogleUtilities/Environment (~> 8.1) | ||||||
|  |     - GoogleUtilities/UserDefaults (~> 8.1) | ||||||
|  |     - nanopb (~> 3.30910.0) | ||||||
|  |     - PromisesSwift (~> 2.1) | ||||||
|   - flutter_inappwebview_macos (0.0.1): |   - flutter_inappwebview_macos (0.0.1): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|     - OrderedSet (~> 6.0.3) |     - OrderedSet (~> 6.0.3) | ||||||
| @@ -63,6 +114,28 @@ PODS: | |||||||
|   - gal (1.0.0): |   - gal (1.0.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|  |   - GoogleAppMeasurement/Core (12.0.0): | ||||||
|  |     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/Network (~> 8.1) | ||||||
|  |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|  |     - nanopb (~> 3.30910.0) | ||||||
|  |   - GoogleAppMeasurement/Default (12.0.0): | ||||||
|  |     - GoogleAdsOnDeviceConversion (= 2.1.0) | ||||||
|  |     - GoogleAppMeasurement/Core (= 12.0.0) | ||||||
|  |     - GoogleAppMeasurement/IdentitySupport (= 12.0.0) | ||||||
|  |     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/Network (~> 8.1) | ||||||
|  |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|  |     - nanopb (~> 3.30910.0) | ||||||
|  |   - GoogleAppMeasurement/IdentitySupport (12.0.0): | ||||||
|  |     - GoogleAppMeasurement/Core (= 12.0.0) | ||||||
|  |     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||||
|  |     - GoogleUtilities/Network (~> 8.1) | ||||||
|  |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|  |     - nanopb (~> 3.30910.0) | ||||||
|   - GoogleDataTransport (10.1.0): |   - GoogleDataTransport (10.1.0): | ||||||
|     - nanopb (~> 3.30910.0) |     - nanopb (~> 3.30910.0) | ||||||
|     - PromisesObjC (~> 2.4) |     - PromisesObjC (~> 2.4) | ||||||
| @@ -76,6 +149,9 @@ PODS: | |||||||
|   - GoogleUtilities/Logger (8.1.0): |   - GoogleUtilities/Logger (8.1.0): | ||||||
|     - GoogleUtilities/Environment |     - GoogleUtilities/Environment | ||||||
|     - GoogleUtilities/Privacy |     - GoogleUtilities/Privacy | ||||||
|  |   - GoogleUtilities/MethodSwizzler (8.1.0): | ||||||
|  |     - GoogleUtilities/Logger | ||||||
|  |     - GoogleUtilities/Privacy | ||||||
|   - GoogleUtilities/Network (8.1.0): |   - GoogleUtilities/Network (8.1.0): | ||||||
|     - GoogleUtilities/Logger |     - GoogleUtilities/Logger | ||||||
|     - "GoogleUtilities/NSData+zlib" |     - "GoogleUtilities/NSData+zlib" | ||||||
| @@ -117,6 +193,8 @@ PODS: | |||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - PromisesObjC (2.4.0) |   - PromisesObjC (2.4.0) | ||||||
|  |   - PromisesSwift (2.4.0): | ||||||
|  |     - PromisesObjC (= 2.4.0) | ||||||
|   - record_macos (1.0.0): |   - record_macos (1.0.0): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - SAMKeychain (1.5.3) |   - SAMKeychain (1.5.3) | ||||||
| @@ -130,25 +208,25 @@ PODS: | |||||||
|   - sqflite_darwin (0.0.4): |   - sqflite_darwin (0.0.4): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - sqlite3 (3.50.3): |   - sqlite3 (3.50.4): | ||||||
|     - sqlite3/common (= 3.50.3) |     - sqlite3/common (= 3.50.4) | ||||||
|   - sqlite3/common (3.50.3) |   - sqlite3/common (3.50.4) | ||||||
|   - sqlite3/dbstatvtab (3.50.3): |   - sqlite3/dbstatvtab (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/fts5 (3.50.3): |   - sqlite3/fts5 (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/math (3.50.3): |   - sqlite3/math (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/perf-threadsafe (3.50.3): |   - sqlite3/perf-threadsafe (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/rtree (3.50.3): |   - sqlite3/rtree (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3/session (3.50.3): |   - sqlite3/session (3.50.4): | ||||||
|     - sqlite3/common |     - sqlite3/common | ||||||
|   - sqlite3_flutter_libs (0.0.1): |   - sqlite3_flutter_libs (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|     - sqlite3 (~> 3.50.3) |     - sqlite3 (~> 3.50.4) | ||||||
|     - sqlite3/dbstatvtab |     - sqlite3/dbstatvtab | ||||||
|     - sqlite3/fts5 |     - sqlite3/fts5 | ||||||
|     - sqlite3/math |     - sqlite3/math | ||||||
| @@ -172,7 +250,9 @@ DEPENDENCIES: | |||||||
|   - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) |   - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) | ||||||
|   - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) |   - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) | ||||||
|   - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) |   - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) | ||||||
|  |   - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) | ||||||
|   - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) |   - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) | ||||||
|  |   - firebase_crashlytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos`) | ||||||
|   - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) |   - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) | ||||||
|   - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) |   - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) | ||||||
|   - flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`) |   - flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`) | ||||||
| @@ -204,15 +284,22 @@ DEPENDENCIES: | |||||||
| SPEC REPOS: | SPEC REPOS: | ||||||
|   trunk: |   trunk: | ||||||
|     - Firebase |     - Firebase | ||||||
|  |     - FirebaseAnalytics | ||||||
|     - FirebaseCore |     - FirebaseCore | ||||||
|  |     - FirebaseCoreExtension | ||||||
|     - FirebaseCoreInternal |     - FirebaseCoreInternal | ||||||
|  |     - FirebaseCrashlytics | ||||||
|     - FirebaseInstallations |     - FirebaseInstallations | ||||||
|     - FirebaseMessaging |     - FirebaseMessaging | ||||||
|  |     - FirebaseRemoteConfigInterop | ||||||
|  |     - FirebaseSessions | ||||||
|  |     - GoogleAppMeasurement | ||||||
|     - GoogleDataTransport |     - GoogleDataTransport | ||||||
|     - GoogleUtilities |     - GoogleUtilities | ||||||
|     - nanopb |     - nanopb | ||||||
|     - OrderedSet |     - OrderedSet | ||||||
|     - PromisesObjC |     - PromisesObjC | ||||||
|  |     - PromisesSwift | ||||||
|     - SAMKeychain |     - SAMKeychain | ||||||
|     - sqlite3 |     - sqlite3 | ||||||
|     - WebRTC-SDK |     - WebRTC-SDK | ||||||
| @@ -230,8 +317,12 @@ EXTERNAL SOURCES: | |||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos |     :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos | ||||||
|   file_selector_macos: |   file_selector_macos: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos |     :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos | ||||||
|  |   firebase_analytics: | ||||||
|  |     :path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos | ||||||
|   firebase_core: |   firebase_core: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos |     :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos | ||||||
|  |   firebase_crashlytics: | ||||||
|  |     :path: Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos | ||||||
|   firebase_messaging: |   firebase_messaging: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos |     :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos | ||||||
|   flutter_inappwebview_macos: |   flutter_inappwebview_macos: | ||||||
| @@ -295,12 +386,19 @@ SPEC CHECKSUMS: | |||||||
|   file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a |   file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a | ||||||
|   file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 |   file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 | ||||||
|   Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 |   Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 | ||||||
|  |   firebase_analytics: 53f0dc87ad10f56a6df8746da60d8a5fe41f886f | ||||||
|   firebase_core: eeea10f64026b68cd0bc3dee079ab4717e22909e |   firebase_core: eeea10f64026b68cd0bc3dee079ab4717e22909e | ||||||
|  |   firebase_crashlytics: 7be1dacc38809971354def57193b280636a3d51a | ||||||
|   firebase_messaging: 5eefcd5bde556bfacdd9968e11c52f39032dfbe5 |   firebase_messaging: 5eefcd5bde556bfacdd9968e11c52f39032dfbe5 | ||||||
|  |   FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7 | ||||||
|   FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a |   FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a | ||||||
|  |   FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361 | ||||||
|   FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 |   FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 | ||||||
|  |   FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7 | ||||||
|   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 |   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 | ||||||
|   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde |   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde | ||||||
|  |   FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613 | ||||||
|  |   FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42 | ||||||
|   flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d |   flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d | ||||||
|   flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 |   flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 | ||||||
|   flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 |   flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 | ||||||
| @@ -309,6 +407,7 @@ SPEC CHECKSUMS: | |||||||
|   flutter_webrtc: 0d70bd8782c19bde286dc52f766eebbea26de201 |   flutter_webrtc: 0d70bd8782c19bde286dc52f766eebbea26de201 | ||||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 |   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 |   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||||
|  |   GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3 | ||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 |   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||||
|   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba |   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba | ||||||
| @@ -322,14 +421,15 @@ SPEC CHECKSUMS: | |||||||
|   pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 |   pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 | ||||||
|   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 |   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 |   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||||
|  |   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 | ||||||
|   record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0 |   record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0 | ||||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c |   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||||
|   share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc |   share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc | ||||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 |   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||||
|   sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e |   sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e | ||||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 |   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||||
|   sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 |   sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b | ||||||
|   sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e |   sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 | ||||||
|   super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 |   super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 | ||||||
|   url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 |   url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 | ||||||
|   volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd |   volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd | ||||||
|   | |||||||
| @@ -234,6 +234,7 @@ | |||||||
| 				3399D490228B24CF009A79C7 /* ShellScript */, | 				3399D490228B24CF009A79C7 /* ShellScript */, | ||||||
| 				F1E275A871246799FC3019F6 /* [CP] Embed Pods Frameworks */, | 				F1E275A871246799FC3019F6 /* [CP] Embed Pods Frameworks */, | ||||||
| 				8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */, | 				8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */, | ||||||
|  | 				6B512DBE9D8E74A09686E70F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, | ||||||
| 			); | 			); | ||||||
| 			buildRules = ( | 			buildRules = ( | ||||||
| 			); | 			); | ||||||
| @@ -376,6 +377,24 @@ | |||||||
| 			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; | 			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; | ||||||
| 			showEnvVarsInLog = 0; | 			showEnvVarsInLog = 0; | ||||||
| 		}; | 		}; | ||||||
|  | 		6B512DBE9D8E74A09686E70F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { | ||||||
|  | 			isa = PBXShellScriptBuildPhase; | ||||||
|  | 			buildActionMask = 2147483647; | ||||||
|  | 			files = ( | ||||||
|  | 			); | ||||||
|  | 			inputFileListPaths = ( | ||||||
|  | 			); | ||||||
|  | 			inputPaths = ( | ||||||
|  | 			); | ||||||
|  | 			name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; | ||||||
|  | 			outputFileListPaths = ( | ||||||
|  | 			); | ||||||
|  | 			outputPaths = ( | ||||||
|  | 			); | ||||||
|  | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|  | 			shellPath = /bin/sh; | ||||||
|  | 			shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n  # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n  DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n  PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n  PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=macos --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; | ||||||
|  | 		}; | ||||||
| 		8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */ = { | 		8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */ = { | ||||||
| 			isa = PBXShellScriptBuildPhase; | 			isa = PBXShellScriptBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
|   | |||||||
							
								
								
									
										86
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										86
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -313,6 +313,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.1" |     version: "2.0.1" | ||||||
|  |   console: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: console | ||||||
|  |       sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.1.0" | ||||||
|   convert: |   convert: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -557,10 +565,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: file_picker |       name: file_picker | ||||||
|       sha256: "8f9f429998f9232d65bc4757af74475ce44fc80f10704ff5dfa8b1d14fc429b9" |       sha256: "970d33d79e1da667b6da222575fd7f2e30e323ca76251504477e6d51405b2d9a" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.2.3" |     version: "10.2.4" | ||||||
|   file_selector_linux: |   file_selector_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -593,6 +601,30 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.9.3+4" |     version: "0.9.3+4" | ||||||
|  |   firebase_analytics: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: firebase_analytics | ||||||
|  |       sha256: "07146e89e11302c6b07e3465c2c556ebcdd0053a3c5b1aa9bfd3203b778e5b4c" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "12.0.0" | ||||||
|  |   firebase_analytics_platform_interface: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: firebase_analytics_platform_interface | ||||||
|  |       sha256: "27e81a0efc821bec6cba64abc1083b91c8ddbad28eeb4c6f6b7c78a59d06f259" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "5.0.0" | ||||||
|  |   firebase_analytics_web: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: firebase_analytics_web | ||||||
|  |       sha256: "7d87f47462042a7d9125e3123db2783bc72917d85e2719d4cb6aeaec209605e1" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.6.0" | ||||||
|   firebase_core: |   firebase_core: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -617,6 +649,22 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.0" |     version: "3.0.0" | ||||||
|  |   firebase_crashlytics: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: firebase_crashlytics | ||||||
|  |       sha256: "95b6871850b1a7e3b09c284c59a0c71fafcad3eee8ac1b6f06aaf8979290cbb8" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "5.0.0" | ||||||
|  |   firebase_crashlytics_platform_interface: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: firebase_crashlytics_platform_interface | ||||||
|  |       sha256: ba5b7a916f1ebedc6db35b33abdc618f202fc25e0792088dfba698e19fec9c09 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.8.11" | ||||||
|   firebase_messaging: |   firebase_messaging: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -662,6 +710,14 @@ packages: | |||||||
|     description: flutter |     description: flutter | ||||||
|     source: sdk |     source: sdk | ||||||
|     version: "0.0.0" |     version: "0.0.0" | ||||||
|  |   flutter_app_update: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: flutter_app_update | ||||||
|  |       sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.2.2" | ||||||
|   flutter_blurhash: |   flutter_blurhash: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1061,6 +1117,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.1" |     version: "3.0.1" | ||||||
|  |   get_it: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: get_it | ||||||
|  |       sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "8.2.0" | ||||||
|   glob: |   glob: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1422,7 +1486,7 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.12.17" |     version: "0.12.17" | ||||||
|   material_color_utilities: |   material_color_utilities: | ||||||
|     dependency: transitive |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: material_color_utilities |       name: material_color_utilities | ||||||
|       sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec |       sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec | ||||||
| @@ -1533,6 +1597,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.0" |     version: "3.0.0" | ||||||
|  |   msix: | ||||||
|  |     dependency: "direct dev" | ||||||
|  |     description: | ||||||
|  |       name: msix | ||||||
|  |       sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.16.12" | ||||||
|   native_exif: |   native_exif: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1973,6 +2045,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     version: "2.1.0" | ||||||
|  |   screenshot: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: screenshot | ||||||
|  |       sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.0.0" | ||||||
|   scroll_to_index: |   scroll_to_index: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								pubspec.yaml
									
									
									
									
									
								
							| @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 3.1.0+120 | version: 3.2.0+124 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.7.2 |   sdk: ^3.7.2 | ||||||
| @@ -73,7 +73,7 @@ dependencies: | |||||||
|     git: https://github.com/LittleSheep2Code/tus_client.git |     git: https://github.com/LittleSheep2Code/tus_client.git | ||||||
|   cross_file: ^0.3.4+2 |   cross_file: ^0.3.4+2 | ||||||
|   image_picker: ^1.1.2 |   image_picker: ^1.1.2 | ||||||
|   file_picker: ^10.2.3 |   file_picker: ^10.2.4 | ||||||
|   riverpod_annotation: ^2.6.1 |   riverpod_annotation: ^2.6.1 | ||||||
|   image_picker_platform_interface: ^2.10.1 |   image_picker_platform_interface: ^2.10.1 | ||||||
|   image_picker_android: ^0.8.12+25 |   image_picker_android: ^0.8.12+25 | ||||||
| @@ -133,6 +133,11 @@ dependencies: | |||||||
|   flutter_typeahead: ^5.2.0 |   flutter_typeahead: ^5.2.0 | ||||||
|   flutter_langdetect: ^0.0.2 |   flutter_langdetect: ^0.0.2 | ||||||
|   waveform_flutter: ^1.2.0 |   waveform_flutter: ^1.2.0 | ||||||
|  |   flutter_app_update: ^3.2.2 | ||||||
|  |   firebase_crashlytics: ^5.0.0 | ||||||
|  |   firebase_analytics: ^12.0.0 | ||||||
|  |   material_color_utilities: ^0.11.1 | ||||||
|  |   screenshot: ^3.0.0 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
| @@ -152,6 +157,7 @@ dev_dependencies: | |||||||
|   riverpod_lint: ^2.6.5 |   riverpod_lint: ^2.6.5 | ||||||
|   drift_dev: ^2.28.0 |   drift_dev: ^2.28.0 | ||||||
|   flutter_launcher_icons: ^0.14.4 |   flutter_launcher_icons: ^0.14.4 | ||||||
|  |   msix: ^3.16.12 | ||||||
|  |  | ||||||
| # For information on the generic Dart part of this file, see the | # For information on the generic Dart part of this file, see the | ||||||
| # following page: https://dart.dev/tools/pub/pubspec | # following page: https://dart.dev/tools/pub/pubspec | ||||||
| @@ -221,3 +227,11 @@ flutter_native_splash: | |||||||
|   image_dark: "assets/icons/icon-dark.png" |   image_dark: "assets/icons/icon-dark.png" | ||||||
|   color: "#ffffff" |   color: "#ffffff" | ||||||
|   color_dark: "#121212" |   color_dark: "#121212" | ||||||
|  |  | ||||||
|  | msix_config: | ||||||
|  |   display_name: Solian | ||||||
|  |   publisher_display_name: Solsynth LLC | ||||||
|  |   identity_name: dev.solian.app | ||||||
|  |   msix_version: 3.2.0.0 | ||||||
|  |   logo_path: .\assets\icons\icon.png | ||||||
|  |   capabilities: internetClientServer, location, microphone, webcam | ||||||
							
								
								
									
										52
									
								
								setup.iss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								setup.iss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | ; ================================================== | ||||||
|  | #define AppVersion "3.2.0" | ||||||
|  | #define BuildNumber "124" | ||||||
|  | ; ================================================== | ||||||
|  |  | ||||||
|  | #define FullVersion AppVersion + "." + BuildNumber | ||||||
|  |  | ||||||
|  | [Setup] | ||||||
|  | AppName=Solian | ||||||
|  | AppVersion={#AppVersion} | ||||||
|  | AppPublisher=Solsynth | ||||||
|  | AppPublisherURL=https://solsynth.dev | ||||||
|  | AppSupportURL=https://kb.solsynth.dev/zh/solar-network | ||||||
|  | AppUpdatesURL=https://github.com/Solsynth/Solian/releases | ||||||
|  | AppCopyright=Copyright © 2025 Solsynth | ||||||
|  | VersionInfoVersion={#FullVersion} | ||||||
|  | UninstallDisplayName=Solian | ||||||
|  | UninstallDisplayIcon={app}\Solian.exe | ||||||
|  |  | ||||||
|  | DefaultDirName={commonpf}\Solian | ||||||
|  | UsePreviousAppDir=no | ||||||
|  |  | ||||||
|  | OutputDir=.\Installer | ||||||
|  | OutputBaseFilename=windows-x86_64-setup | ||||||
|  | SetupIconFile=.\assets\icons\icon.ico   | ||||||
|  |  | ||||||
|  | Compression=lzma2/ultra64 | ||||||
|  | SolidCompression=yes | ||||||
|  | LZMAUseSeparateProcess=yes | ||||||
|  | LZMANumBlockThreads=4 | ||||||
|  |  | ||||||
|  | ArchitecturesAllowed=x64compatible | ||||||
|  | PrivilegesRequired=admin | ||||||
|  |  | ||||||
|  | [Files] | ||||||
|  | Source: ".\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs | ||||||
|  |  | ||||||
|  | [Icons] | ||||||
|  | Name: "{group}\Solian"; Filename: "{app}\Solian.exe";IconFilename: "{app}\Solian.exe" | ||||||
|  | Name: "{group}\{cm:UninstallProgram,Solian}"; Filename: "{uninstallexe}" | ||||||
|  | Name: "{autodesktop}\Solian"; Filename: "{app}\Solian.exe"; Tasks: desktopicon | ||||||
|  |  | ||||||
|  | [Tasks] | ||||||
|  | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked | ||||||
|  |  | ||||||
|  | [Run] | ||||||
|  | Filename: "{app}\Solian.exe"; Description: "Launch Solian"; Flags: nowait postinstall skipifsilent | ||||||
|  |  | ||||||
|  | [UninstallDelete] | ||||||
|  | Type: filesandordirs; Name: "{userappdata}\dev.solsynth\Solian" | ||||||
|  | Type: files; Name: "{group}\Solian.lnk" ; | ||||||
|  | Type: files; Name: "{autodesktop}\Solian.lnk" ; | ||||||
		Reference in New Issue
	
	Block a user