Compare commits
	
		
			72 Commits
		
	
	
		
			c1ec6f0849
			...
			3.2.0+134
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 28b6eade48 | |||
| 1de7ef8c96 | |||
| 67eac5dcf5 | |||
| 7a44bfa075 | |||
| 1c2f25a152 | |||
| be26ea280e | |||
| b4996d069f | |||
| bf4892b34d | |||
| 5f84751fd5 | |||
| 457d1bac60 | |||
| 02ec11845b | |||
| 612f1bf004 | |||
| fd80b713ad | |||
| 508805368c | |||
| 98eb28a4ec | |||
| d1a2f59dd1 | |||
| bb9adb963a | |||
| 83e40cd860 | |||
| c06fb12f6a | |||
| 6600cf4df8 | |||
| 4293daaa2f | |||
| 866674ddde | |||
| 27d478ba4f | |||
| cccade763f | |||
| f760b85186 | |||
| e68c5f4f92 | |||
| b0f3b6b5c3 | |||
| cb2af379fa | |||
| 38f8103265 | |||
| 06bb18bdaa | |||
| 84c38500d0 | |||
| 9529bbf08b | |||
| 8baf77bcf7 | |||
| b2ac5fbef2 | |||
| c79b1d7aab | |||
|  | 4f55a8209c | ||
|  | ace302111a | ||
|  | 1391fa0dde | ||
|  | cbdc7acdcd | ||
|  | b80d91825a | ||
|  | 1a703b7eba | ||
|  | 3621ea7744 | ||
|  | b638343f02 | ||
|  | 269a64cabb | ||
| 406e5187a8 | |||
| 9bdd08d8dd | |||
| d737232dcf | |||
| c9d751479e | |||
| a2c2bfe585 | |||
| c7f9da0dee | |||
|  | a243cda1df | ||
|  | 7b238f32fd | ||
| 313af28d7f | |||
| c64e1e208c | |||
| c9b07a9a2a | |||
| 55c0e355f1 | |||
| be414891ec | |||
| 787876ab6a | |||
| 8578cde620 | |||
| 14d55d45a8 | |||
| 724391584e | |||
| 3a5e45808a | |||
| 488055955c | |||
|  | 313ebc64cc | ||
|  | 1ed8b1d0c1 | ||
| 4af816d931 | |||
| 1c058a4323 | |||
| 461ed1fcda | |||
| 5363afa558 | |||
| f0d2737da8 | |||
| 1f2f80aa3e | |||
| 240a872e65 | 
							
								
								
									
										4
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -29,10 +29,8 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - name: Clone repository |       - name: Clone repository | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|       # https://github.com/MaikuB/flutter_local_notifications/issues/2615#issuecomment-2971286732 |  | ||||||
|       # Use the 2.20.0 version of the action might solve the flutter_local_notifications windows build crash issue |  | ||||||
|       - name: Set up Flutter |       - name: Set up Flutter | ||||||
|         uses: subosito/flutter-action@v2.20.0 |         uses: subosito/flutter-action@v2 | ||||||
|         with: |         with: | ||||||
|           channel: stable |           channel: stable | ||||||
|           cache: true |           cache: true | ||||||
|   | |||||||
| @@ -30,6 +30,8 @@ | |||||||
|   "fieldEmailAddressMustBeValid": "The email address must be valid.", |   "fieldEmailAddressMustBeValid": "The email address must be valid.", | ||||||
|   "logout": "Logout", |   "logout": "Logout", | ||||||
|   "updateYourProfile": "Profile Settings", |   "updateYourProfile": "Profile Settings", | ||||||
|  |   "settingsDefaultPool": "Default file pool", | ||||||
|  |   "settingsDefaultPoolHelper": "Select the default storage pool for file uploads", | ||||||
|   "accountBasicInfo": "Basic Info", |   "accountBasicInfo": "Basic Info", | ||||||
|   "accountProfile": "Your Profile", |   "accountProfile": "Your Profile", | ||||||
|   "saveChanges": "Save Changes", |   "saveChanges": "Save Changes", | ||||||
| @@ -168,6 +170,7 @@ | |||||||
|   "addPhoto": "Add photo", |   "addPhoto": "Add photo", | ||||||
|   "addAudio": "Add audio", |   "addAudio": "Add audio", | ||||||
|   "addFile": "Add file", |   "addFile": "Add file", | ||||||
|  |   "uploadFile": "Upload File", | ||||||
|   "recordAudio": "Record Audio", |   "recordAudio": "Record Audio", | ||||||
|   "linkAttachment": "Link Attachment", |   "linkAttachment": "Link Attachment", | ||||||
|   "fileIdCannotBeEmpty": "File ID cannot be empty", |   "fileIdCannotBeEmpty": "File ID cannot be empty", | ||||||
| @@ -261,14 +264,14 @@ | |||||||
|   "createStickerPack": "Create a Sticker Pack", |   "createStickerPack": "Create a Sticker Pack", | ||||||
|   "editStickerPack": "Edit Sticker Pack", |   "editStickerPack": "Edit Sticker Pack", | ||||||
|   "deleteStickerPack": "Delete Sticker Pack", |   "deleteStickerPack": "Delete Sticker Pack", | ||||||
|   "deleteStickerPackHint": "Are you sure to delete this sticker pack? This action cannot be undone.", |   "deleteStickerPackHint": "Are you sure you want to delete this sticker pack? This action cannot be undone.", | ||||||
|   "stickerPackPrefix": "Prefix", |   "stickerPackPrefix": "Prefix", | ||||||
|   "stickerPackPrefixHint": "The prefix will be added before each stickers' slug in this pack.", |   "stickerPackPrefixHint": "The prefix will be added before each stickers' slug in this pack.", | ||||||
|   "stickers": "Stickers", |   "stickers": "Stickers", | ||||||
|   "createSticker": "Create a Sticker", |   "createSticker": "Create a Sticker", | ||||||
|   "editSticker": "Edit Sticker", |   "editSticker": "Edit Sticker", | ||||||
|   "deleteSticker": "Delete Sticker", |   "deleteSticker": "Delete Sticker", | ||||||
|   "deleteStickerHint": "Are you sure to delete this sticker? This action cannot be undone.", |   "deleteStickerHint": "Are you sure you want to delete this sticker? This action cannot be undone.", | ||||||
|   "stickerImage": "Image", |   "stickerImage": "Image", | ||||||
|   "stickerSlug": "Slug", |   "stickerSlug": "Slug", | ||||||
|   "stickerSlugHint": "The slug will be combined with the prefix to form the sticker's unique identifier.", |   "stickerSlugHint": "The slug will be combined with the prefix to form the sticker's unique identifier.", | ||||||
| @@ -333,13 +336,25 @@ | |||||||
|   "levelingProgress": "Leveling Progress", |   "levelingProgress": "Leveling Progress", | ||||||
|   "levelingProgressExperience": "{} EXP", |   "levelingProgressExperience": "{} EXP", | ||||||
|   "levelingProgressLevel": "Level {}", |   "levelingProgressLevel": "Level {}", | ||||||
|  |   "levelingStage1": "Novice", | ||||||
|  |   "levelingStage2": "Apprentice", | ||||||
|  |   "levelingStage3": "Journeyman", | ||||||
|  |   "levelingStage4": "Adept", | ||||||
|  |   "levelingStage5": "Expert", | ||||||
|  |   "levelingStage6": "Master", | ||||||
|  |   "levelingStage7": "Grandmaster", | ||||||
|  |   "levelingStage8": "Legend", | ||||||
|  |   "levelingStage9": "Myth", | ||||||
|  |   "levelingStage10": "Immortal", | ||||||
|  |   "levelingStage11": "Divine", | ||||||
|  |   "levelingStage12": "Transcendent", | ||||||
|   "fileUploadingProgress": "Uploading file #{}: {}%", |   "fileUploadingProgress": "Uploading file #{}: {}%", | ||||||
|   "removeChatMember": "Remove Chat Room Member", |   "removeChatMember": "Remove Chat Room Member", | ||||||
|   "removeChatMemberHint": "Are you sure to remove this member from the room?", |   "removeChatMemberHint": "Are you sure you want to remove this member from the room?", | ||||||
|   "removeRealmMember": "Remove Realm Member", |   "removeRealmMember": "Remove Realm Member", | ||||||
|   "removeRealmMemberHint": "Are you sure to remove this member from the realm?", |   "removeRealmMemberHint": "Are you sure you want to remove this member from the realm?", | ||||||
|   "removePublisherMember": "Remove Publisher Member", |   "removePublisherMember": "Remove Publisher Member", | ||||||
|   "removePublisherMemberHint": "Are you sure to remove this member from the publisher?", |   "removePublisherMemberHint": "Are you sure you want to remove this member from the publisher?", | ||||||
|   "memberRole": "Member Role", |   "memberRole": "Member Role", | ||||||
|   "memberRoleHint": "Greater number has higher permission.", |   "memberRoleHint": "Greater number has higher permission.", | ||||||
|   "memberRoleEdit": "Edit role for @{}", |   "memberRoleEdit": "Edit role for @{}", | ||||||
| @@ -348,9 +363,9 @@ | |||||||
|   "brokenLink": "Unable open link {}... It might be broken or missing uri parts...", |   "brokenLink": "Unable open link {}... It might be broken or missing uri parts...", | ||||||
|   "copyToClipboard": "Copy to clipboard", |   "copyToClipboard": "Copy to clipboard", | ||||||
|   "leaveChatRoom": "Leave Chat Room", |   "leaveChatRoom": "Leave Chat Room", | ||||||
|   "leaveChatRoomHint": "Are you sure to leave this chat room?", |   "leaveChatRoomHint": "Are you sure you want to leave this chat room?", | ||||||
|   "leaveRealm": "Leave Realm", |   "leaveRealm": "Leave Realm", | ||||||
|   "leaveRealmHint": "Are you sure to leave this realm?", |   "leaveRealmHint": "Are you sure you want to leave this realm?", | ||||||
|   "walletNotFound": "Wallet not found", |   "walletNotFound": "Wallet not found", | ||||||
|   "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.", |   "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.", | ||||||
|   "walletCreate": "Create a Wallet", |   "walletCreate": "Create a Wallet", | ||||||
| @@ -447,11 +462,13 @@ | |||||||
|   "lastActiveAt": "Last active at {}", |   "lastActiveAt": "Last active at {}", | ||||||
|   "authDeviceLogout": "Logout", |   "authDeviceLogout": "Logout", | ||||||
|   "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.", |   "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.", | ||||||
|  |   "authDeviceChallenges": "Device Usage", | ||||||
|  |   "authDeviceHint": "Swipe left to edit label, swipe right to logout device.", | ||||||
|   "typingHint": { |   "typingHint": { | ||||||
|     "one": "{} is typing...", |     "one": "{} is typing...", | ||||||
|     "other": "{} are typing..." |     "other": "{} are typing..." | ||||||
|   }, |   }, | ||||||
|   "authDeviceEditLabel": "Edit Label", |   "authDeviceEditLabel": "Edit Device Label", | ||||||
|   "authDeviceLabelTitle": "Edit Device Label", |   "authDeviceLabelTitle": "Edit Device Label", | ||||||
|   "authDeviceLabelHint": "Enter a name for this device", |   "authDeviceLabelHint": "Enter a name for this device", | ||||||
|   "authDeviceSwipeEditHint": "Swipe left to edit label", |   "authDeviceSwipeEditHint": "Swipe left to edit label", | ||||||
| @@ -468,6 +485,7 @@ | |||||||
|   "settingsKeyboardShortcutSettings": "Settings", |   "settingsKeyboardShortcutSettings": "Settings", | ||||||
|   "settingsKeyboardShortcutNewMessage": "New Message", |   "settingsKeyboardShortcutNewMessage": "New Message", | ||||||
|   "settingsKeyboardShortcutCloseDialog": "Close Dialog", |   "settingsKeyboardShortcutCloseDialog": "Close Dialog", | ||||||
|  |   "settingsMessageDisplayStyle": "Message Display Style", | ||||||
|   "close": "Close", |   "close": "Close", | ||||||
|   "drafts": "Drafts", |   "drafts": "Drafts", | ||||||
|   "noDrafts": "No drafts yet", |   "noDrafts": "No drafts yet", | ||||||
| @@ -518,7 +536,7 @@ | |||||||
|   "contactMethodPrimary": "Primary", |   "contactMethodPrimary": "Primary", | ||||||
|   "contactMethodSetPrimary": "Set as Primary", |   "contactMethodSetPrimary": "Set as Primary", | ||||||
|   "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications", |   "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications", | ||||||
|   "contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.", |   "contactMethodDeleteHint": "Are you sure you want to delete this contact method? This action cannot be undone.", | ||||||
|   "contactMethodMakePublic": "Make Public", |   "contactMethodMakePublic": "Make Public", | ||||||
|   "contactMethodMakePrivate": "Make Private", |   "contactMethodMakePrivate": "Make Private", | ||||||
|   "contactMethodPublic": "Public", |   "contactMethodPublic": "Public", | ||||||
| @@ -552,6 +570,7 @@ | |||||||
|   "checkInResultT2": "Mid", |   "checkInResultT2": "Mid", | ||||||
|   "checkInResultT3": "Good", |   "checkInResultT3": "Good", | ||||||
|   "checkInResultT4": "Best", |   "checkInResultT4": "Best", | ||||||
|  |   "checkInResultT5": "Birthday", | ||||||
|   "accountProfileView": "View Profile", |   "accountProfileView": "View Profile", | ||||||
|   "unspecified": "Unspecified", |   "unspecified": "Unspecified", | ||||||
|   "added": "Added", |   "added": "Added", | ||||||
| @@ -644,8 +663,6 @@ | |||||||
|   "abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", |   "abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", | ||||||
|   "abuseReportError": "Failed to submit report. Please try again.", |   "abuseReportError": "Failed to submit report. Please try again.", | ||||||
|   "abuseReportReasonRequired": "Please provide details about the issue", |   "abuseReportReasonRequired": "Please provide details about the issue", | ||||||
|   "abuseReportSuccessTitle": "Report Submitted", |  | ||||||
|   "abuseReportErrorTitle": "Error", |  | ||||||
|   "abuseReportTypeSpam": "Spam or Misleading", |   "abuseReportTypeSpam": "Spam or Misleading", | ||||||
|   "abuseReportTypeHarassment": "Harassment or Abuse", |   "abuseReportTypeHarassment": "Harassment or Abuse", | ||||||
|   "abuseReportTypeInappropriate": "Inappropriate Content", |   "abuseReportTypeInappropriate": "Inappropriate Content", | ||||||
| @@ -827,11 +844,6 @@ | |||||||
|   "postCategorySports": "Sports", |   "postCategorySports": "Sports", | ||||||
|   "postCategoryFinance": "Finance", |   "postCategoryFinance": "Finance", | ||||||
|   "postCategoryLife": "Life", |   "postCategoryLife": "Life", | ||||||
|   "postCategoryArt": "Art", |  | ||||||
|   "postCategoryStudy": "Study", |  | ||||||
|   "postCategoryGaming": "Gaming", |  | ||||||
|   "postCategoryProgramming": "Programming", |  | ||||||
|   "postCategoryMusic": "Music", |  | ||||||
|   "links": "Links", |   "links": "Links", | ||||||
|   "addLink": "Add link", |   "addLink": "Add link", | ||||||
|   "linkKey": "Link Name", |   "linkKey": "Link Name", | ||||||
| @@ -890,6 +902,15 @@ | |||||||
|   "attachmentOnDevice": "On-device", |   "attachmentOnDevice": "On-device", | ||||||
|   "attachmentOnCloud": "On-cloud", |   "attachmentOnCloud": "On-cloud", | ||||||
|   "attachments": "Attachments", |   "attachments": "Attachments", | ||||||
|  |   "uploadAttachment": "Upload Attachment", | ||||||
|  |   "attachmentPreview": "Attachment Preview", | ||||||
|  |   "selectPool": "Select Pool", | ||||||
|  |   "choosePool": "Choose a pool", | ||||||
|  |   "errorLoadingPools": "Error loading pools", | ||||||
|  |   "quotaCostInfo": "This upload will cost {} quota points", | ||||||
|  |   "uploadConstraints": "Upload Constraints", | ||||||
|  |   "fileSizeExceeded": "File size exceeds the maximum limit of {}", | ||||||
|  |   "fileTypeNotAccepted": "File type is not accepted by this pool", | ||||||
|   "publisherCollabInvitation": "Collabration invitations", |   "publisherCollabInvitation": "Collabration invitations", | ||||||
|   "publisherCollabInvitationCount": { |   "publisherCollabInvitationCount": { | ||||||
|     "zero": "No invitation", |     "zero": "No invitation", | ||||||
| @@ -1006,6 +1027,11 @@ | |||||||
|   "expandPoll": "Expand Poll", |   "expandPoll": "Expand Poll", | ||||||
|   "collapsePoll": "Collapse Poll", |   "collapsePoll": "Collapse Poll", | ||||||
|   "embedView": "Embed View", |   "embedView": "Embed View", | ||||||
|  |   "auto": "Auto", | ||||||
|  |   "manual": "Manual", | ||||||
|  |   "iframeCode": "Iframe Code", | ||||||
|  |   "iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">", | ||||||
|  |   "parseIframe": "Parse Iframe", | ||||||
|   "embedUri": "Embed URI", |   "embedUri": "Embed URI", | ||||||
|   "aspectRatio": "Aspect Ratio", |   "aspectRatio": "Aspect Ratio", | ||||||
|   "renderer": "Renderer", |   "renderer": "Renderer", | ||||||
| @@ -1016,5 +1042,26 @@ | |||||||
|   "currentEmbed": "Current Embed", |   "currentEmbed": "Current Embed", | ||||||
|   "noEmbed": "No embed yet", |   "noEmbed": "No embed yet", | ||||||
|   "save": "Save", |   "save": "Save", | ||||||
|   "webView": "Web View" |   "webView": "Web View", | ||||||
|  |   "messageActions": "Message Actions", | ||||||
|  |   "viewEmbedLoadHint": "Tap to load", | ||||||
|  |   "files": "Files", | ||||||
|  |   "confirmDeleteFile": "Are you sure you want to delete this file?", | ||||||
|  |   "deleteFile": "Delete File", | ||||||
|  |   "failedToDeleteFile": "Failed to delete file", | ||||||
|  |   "drive": "Drive", | ||||||
|  |   "allPools": "All Pools", | ||||||
|  |   "includeRecycled": "Include Recycled", | ||||||
|  |   "confirmDeleteRecycledFiles": "Are you sure you want to delete all recycled files?", | ||||||
|  |   "deleteRecycledFiles": "Delete Recycled Files", | ||||||
|  |   "recycledFilesDeleted": "Recycled files deleted successfully", | ||||||
|  |   "failedToDeleteRecycledFiles": "Failed to delete recycled files", | ||||||
|  |   "upload": "Upload", | ||||||
|  |   "fileInfoTitle": "File Information", | ||||||
|  |   "fileHashCopied": "File hash copied to clipboard", | ||||||
|  |   "fileIdCopied": "File ID copied to clipboard", | ||||||
|  |   "fileNameCopied": "File name copied to clipboard", | ||||||
|  |   "fileMetadata": "File Metadata", | ||||||
|  |   "userMetadata": "User Metadata", | ||||||
|  |   "valueCopied": "Value copied to clipboard" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -122,6 +122,9 @@ | |||||||
|   "addVideo": "添加视频", |   "addVideo": "添加视频", | ||||||
|   "addPhoto": "添加照片", |   "addPhoto": "添加照片", | ||||||
|   "addFile": "添加文件", |   "addFile": "添加文件", | ||||||
|  |   "uploadFile": "上传文件", | ||||||
|  |   "settingsDefaultPool": "选择文件池", | ||||||
|  |   "settingsDefaultPoolHelper": "为文件上传选择一个默认池", | ||||||
|   "createDirectMessage": "创建新私人消息", |   "createDirectMessage": "创建新私人消息", | ||||||
|   "gotoDirectMessage": "前往私信", |   "gotoDirectMessage": "前往私信", | ||||||
|   "react": "反应", |   "react": "反应", | ||||||
| @@ -280,6 +283,18 @@ | |||||||
|   "levelingProgress": "等级进度", |   "levelingProgress": "等级进度", | ||||||
|   "levelingProgressExperience": "{} 经验值", |   "levelingProgressExperience": "{} 经验值", | ||||||
|   "levelingProgressLevel": "等级 {}", |   "levelingProgressLevel": "等级 {}", | ||||||
|  |   "levelingStage1": "新手", | ||||||
|  |   "levelingStage2": "学徒", | ||||||
|  |   "levelingStage3": "熟练工", | ||||||
|  |   "levelingStage4": "行家", | ||||||
|  |   "levelingStage5": "专家", | ||||||
|  |   "levelingStage6": "大师", | ||||||
|  |   "levelingStage7": "宗师", | ||||||
|  |   "levelingStage8": "传奇", | ||||||
|  |   "levelingStage9": "神话", | ||||||
|  |   "levelingStage10": "不朽", | ||||||
|  |   "levelingStage11": "神圣", | ||||||
|  |   "levelingStage12": "超凡", | ||||||
|   "fileUploadingProgress": "正在上传文件 #{}: {}%", |   "fileUploadingProgress": "正在上传文件 #{}: {}%", | ||||||
|   "removeChatMember": "移除聊天室成员", |   "removeChatMember": "移除聊天室成员", | ||||||
|   "removeChatMemberHint": "确定要将此成员从聊天室中移除吗?", |   "removeChatMemberHint": "确定要将此成员从聊天室中移除吗?", | ||||||
|   | |||||||
| @@ -122,6 +122,10 @@ | |||||||
|     "addVideo": "添加視頻", |     "addVideo": "添加視頻", | ||||||
|     "addPhoto": "添加照片", |     "addPhoto": "添加照片", | ||||||
|     "addFile": "添加文件", |     "addFile": "添加文件", | ||||||
|  |     "uploadFile": "上傳文件", | ||||||
|  |     "settingsDefaultPool": "選擇文件池", | ||||||
|  |     "settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池", | ||||||
|  |   | ||||||
|     "createDirectMessage": "創建新私人消息", |     "createDirectMessage": "創建新私人消息", | ||||||
|     "gotoDirectMessage": "前往私信", |     "gotoDirectMessage": "前往私信", | ||||||
|     "react": "反應", |     "react": "反應", | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/angry.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/angry.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.0 MiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/clap.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/clap.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.0 MiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/confuse.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/confuse.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 668 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/party.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/party.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 MiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/pray.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/pray.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 666 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/thumb_up.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/thumb_up.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 623 KiB | 
| @@ -5,3 +5,7 @@ targets: | |||||||
|         options: |         options: | ||||||
|           explicit_to_json: true |           explicit_to_json: true | ||||||
|           field_rename: snake |           field_rename: snake | ||||||
|  |       drift_dev: | ||||||
|  |         options: | ||||||
|  |           databases: | ||||||
|  |             app_database: lib/database/drift_db.dart | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								drift_schemas/app_database/drift_schema_v6.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								drift_schemas/app_database/drift_schema_v6.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -50,18 +50,18 @@ PODS: | |||||||
|   - Firebase/Messaging (12.2.0): |   - Firebase/Messaging (12.2.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseMessaging (~> 12.2.0) |     - FirebaseMessaging (~> 12.2.0) | ||||||
|   - firebase_analytics (12.0.1): |   - firebase_analytics (12.0.2): | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - FirebaseAnalytics (= 12.2.0) |     - FirebaseAnalytics (= 12.2.0) | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_core (4.1.0): |   - firebase_core (4.1.1): | ||||||
|     - Firebase/CoreOnly (= 12.2.0) |     - Firebase/CoreOnly (= 12.2.0) | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_crashlytics (5.0.1): |   - firebase_crashlytics (5.0.2): | ||||||
|     - Firebase/Crashlytics (= 12.2.0) |     - Firebase/Crashlytics (= 12.2.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_messaging (16.0.1): |   - firebase_messaging (16.0.2): | ||||||
|     - Firebase/Messaging (= 12.2.0) |     - Firebase/Messaging (= 12.2.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
| @@ -149,9 +149,9 @@ PODS: | |||||||
|   - flutter_udid (0.0.1): |   - flutter_udid (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - SAMKeychain |     - SAMKeychain | ||||||
|   - flutter_webrtc (1.1.0): |   - flutter_webrtc (1.2.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - WebRTC-SDK (= 137.7151.03) |     - WebRTC-SDK (= 137.7151.04) | ||||||
|   - gal (1.0.0): |   - gal (1.0.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
| @@ -219,7 +219,7 @@ PODS: | |||||||
|   - livekit_client (2.5.0): |   - livekit_client (2.5.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
|     - WebRTC-SDK (= 137.7151.03) |     - WebRTC-SDK (= 137.7151.04) | ||||||
|   - local_auth_darwin (0.0.1): |   - local_auth_darwin (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
| @@ -299,7 +299,7 @@ PODS: | |||||||
|     - Flutter |     - Flutter | ||||||
|   - wakelock_plus (0.0.1): |   - wakelock_plus (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - WebRTC-SDK (137.7151.03) |   - WebRTC-SDK (137.7151.04) | ||||||
|  |  | ||||||
| DEPENDENCIES: | DEPENDENCIES: | ||||||
|   - Alamofire |   - Alamofire | ||||||
| @@ -476,10 +476,10 @@ SPEC CHECKSUMS: | |||||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be |   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||||
|   file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 |   file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 | ||||||
|   Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1 |   Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1 | ||||||
|   firebase_analytics: 111ff65791a430356bd6c7e4d7339537fc6a15ae |   firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e | ||||||
|   firebase_core: 3ff52146406557dddd01d570e807e203ec7e1302 |   firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d | ||||||
|   firebase_crashlytics: 3637078b718a52dc9fb4d64e37c969e86b87ff6f |   firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb | ||||||
|   firebase_messaging: 3dcc998dd98e1e54af75d0cccae8606eba43553c |   firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a | ||||||
|   FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8 |   FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8 | ||||||
|   FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd |   FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd | ||||||
|   FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5 |   FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5 | ||||||
| @@ -499,7 +499,7 @@ SPEC CHECKSUMS: | |||||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 |   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||||
|   flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 |   flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 | ||||||
|   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 |   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 | ||||||
|   flutter_webrtc: b0b2e04411747142962164a1cfa43a1af9a0afac |   flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22 | ||||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 |   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||||
|   GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe |   GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe | ||||||
|   GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af |   GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af | ||||||
| @@ -508,8 +508,8 @@ SPEC CHECKSUMS: | |||||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a |   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 |   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||||
|   Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c |   Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c | ||||||
|   livekit_client: f810c81bbbc229a84f60b09e66603ac4e93f7599 |   livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4 | ||||||
|   local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19 |   local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb | ||||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 |   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 |   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||||
|   nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 |   nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 | ||||||
| @@ -536,7 +536,7 @@ SPEC CHECKSUMS: | |||||||
|   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d |   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d | ||||||
|   volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 |   volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 | ||||||
|   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 |   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 | ||||||
|   WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3 |   WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e | ||||||
|  |  | ||||||
| PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f | PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f | ||||||
|  |  | ||||||
|   | |||||||
| @@ -566,7 +566,7 @@ | |||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 			shellPath = /bin/sh; | 			shellPath = /bin/sh; | ||||||
| 			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; | 			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; | ||||||
| 		}; | 		}; | ||||||
| 		4815E0A19398E51078F4160D /* [CP] Check Pods Manifest.lock */ = { | 		4815E0A19398E51078F4160D /* [CP] Check Pods Manifest.lock */ = { | ||||||
| 			isa = PBXShellScriptBuildPhase; | 			isa = PBXShellScriptBuildPhase; | ||||||
| @@ -883,6 +883,7 @@ | |||||||
| 				); | 				); | ||||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | ||||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
|  | 				SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)"; | ||||||
| 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | ||||||
| 				SWIFT_VERSION = 5.0; | 				SWIFT_VERSION = 5.0; | ||||||
| 				VERSIONING_SYSTEM = "apple-generic"; | 				VERSIONING_SYSTEM = "apple-generic"; | ||||||
| @@ -1096,6 +1097,7 @@ | |||||||
| 				SKIP_INSTALL = YES; | 				SKIP_INSTALL = YES; | ||||||
| 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; | 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; | ||||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||||
|  | 				SWIFT_ENABLE_EXPLICIT_MODULES = NO; | ||||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||||
| 				SWIFT_VERSION = 5.0; | 				SWIFT_VERSION = 5.0; | ||||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||||
| @@ -1137,6 +1139,7 @@ | |||||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
| 				SKIP_INSTALL = YES; | 				SKIP_INSTALL = YES; | ||||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||||
|  | 				SWIFT_ENABLE_EXPLICIT_MODULES = NO; | ||||||
| 				SWIFT_VERSION = 5.0; | 				SWIFT_VERSION = 5.0; | ||||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||||
| 			}; | 			}; | ||||||
| @@ -1177,6 +1180,7 @@ | |||||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
| 				SKIP_INSTALL = YES; | 				SKIP_INSTALL = YES; | ||||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||||
|  | 				SWIFT_ENABLE_EXPLICIT_MODULES = NO; | ||||||
| 				SWIFT_VERSION = 5.0; | 				SWIFT_VERSION = 5.0; | ||||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||||
| 			}; | 			}; | ||||||
| @@ -1434,6 +1438,7 @@ | |||||||
| 				); | 				); | ||||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | ||||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
|  | 				SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)"; | ||||||
| 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | ||||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||||
| 				SWIFT_VERSION = 5.0; | 				SWIFT_VERSION = 5.0; | ||||||
| @@ -1462,6 +1467,7 @@ | |||||||
| 				); | 				); | ||||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | ||||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
|  | 				SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)"; | ||||||
| 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | ||||||
| 				SWIFT_VERSION = 5.0; | 				SWIFT_VERSION = 5.0; | ||||||
| 				VERSIONING_SYSTEM = "apple-generic"; | 				VERSIONING_SYSTEM = "apple-generic"; | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ class AppDatabase extends _$AppDatabase { | |||||||
|   AppDatabase(super.e); |   AppDatabase(super.e); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   int get schemaVersion => 6; |   int get schemaVersion => 7; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   MigrationStrategy get migration => MigrationStrategy( |   MigrationStrategy get migration => MigrationStrategy( | ||||||
| @@ -21,8 +21,8 @@ class AppDatabase extends _$AppDatabase { | |||||||
|     }, |     }, | ||||||
|     onUpgrade: (Migrator m, int from, int to) async { |     onUpgrade: (Migrator m, int from, int to) async { | ||||||
|       if (from < 2) { |       if (from < 2) { | ||||||
|         // Add isRead column with default value false |         // Add isDeleted column with default value false | ||||||
|         await m.addColumn(chatMessages, chatMessages.isRead); |         await m.addColumn(chatMessages, chatMessages.isDeleted); | ||||||
|       } |       } | ||||||
|       if (from < 4) { |       if (from < 4) { | ||||||
|         // Drop old draft tables if they exist |         // Drop old draft tables if they exist | ||||||
| @@ -32,6 +32,29 @@ class AppDatabase extends _$AppDatabase { | |||||||
|         // Migrate from old schema to new schema with separate searchable fields |         // Migrate from old schema to new schema with separate searchable fields | ||||||
|         await _migrateToVersion6(m); |         await _migrateToVersion6(m); | ||||||
|       } |       } | ||||||
|  |       if (from < 7) { | ||||||
|  |         // Add new columns from SnChatMessage, ignore if they already exist | ||||||
|  |         final columnsToAdd = [ | ||||||
|  |           chatMessages.updatedAt, | ||||||
|  |           chatMessages.deletedAt, | ||||||
|  |           chatMessages.type, | ||||||
|  |           chatMessages.meta, | ||||||
|  |           chatMessages.membersMentioned, | ||||||
|  |           chatMessages.editedAt, | ||||||
|  |           chatMessages.attachments, | ||||||
|  |           chatMessages.reactions, | ||||||
|  |           chatMessages.repliedMessageId, | ||||||
|  |           chatMessages.forwardedMessageId, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         for (final column in columnsToAdd) { | ||||||
|  |           try { | ||||||
|  |             await m.addColumn(chatMessages, column); | ||||||
|  |           } catch (e) { | ||||||
|  |             // Column already exists, skip | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -116,12 +139,6 @@ class AppDatabase extends _$AppDatabase { | |||||||
|     )).write(ChatMessagesCompanion(status: Value(status))); |     )).write(ChatMessagesCompanion(status: Value(status))); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<int> markMessageAsRead(String id) { |  | ||||||
|     return (update(chatMessages)..where( |  | ||||||
|       (m) => m.id.equals(id), |  | ||||||
|     )).write(ChatMessagesCompanion(isRead: const Value(true))); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<int> deleteMessage(String id) { |   Future<int> deleteMessage(String id) { | ||||||
|     return (delete(chatMessages)..where((m) => m.id.equals(id))).go(); |     return (delete(chatMessages)..where((m) => m.id.equals(id))).go(); | ||||||
|   } |   } | ||||||
| @@ -134,15 +151,27 @@ class AppDatabase extends _$AppDatabase { | |||||||
|  |  | ||||||
|   Future<List<LocalChatMessage>> searchMessages( |   Future<List<LocalChatMessage>> searchMessages( | ||||||
|     String roomId, |     String roomId, | ||||||
|     String query, |     String query, { | ||||||
|   ) async { |     bool? withAttachments, | ||||||
|  |   }) async { | ||||||
|     var selectStatement = select(chatMessages) |     var selectStatement = select(chatMessages) | ||||||
|       ..where((m) => m.roomId.equals(roomId)); |       ..where((m) => m.roomId.equals(roomId)); | ||||||
|  |  | ||||||
|     if (query.isNotEmpty) { |     if (query.isNotEmpty) { | ||||||
|  |       final searchTerm = '%$query%'; | ||||||
|       selectStatement = |       selectStatement = | ||||||
|           selectStatement |           selectStatement..where( | ||||||
|             ..where((m) => m.content.like('%${query.toLowerCase()}%')); |             (m) => | ||||||
|  |                 m.content.like(searchTerm) | | ||||||
|  |                 m.meta.like(searchTerm) | | ||||||
|  |                 m.attachments.like(searchTerm) | | ||||||
|  |                 m.type.like(searchTerm), | ||||||
|  |           ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (withAttachments == true) { | ||||||
|  |       selectStatement = | ||||||
|  |           selectStatement..where((m) => m.attachments.equals('[]').not()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     final messages = |     final messages = | ||||||
| @@ -154,16 +183,26 @@ class AppDatabase extends _$AppDatabase { | |||||||
|  |  | ||||||
|   // Convert between Drift and model objects |   // Convert between Drift and model objects | ||||||
|   ChatMessagesCompanion messageToCompanion(LocalChatMessage message) { |   ChatMessagesCompanion messageToCompanion(LocalChatMessage message) { | ||||||
|  |     final remote = message.toRemoteMessage(); | ||||||
|     return ChatMessagesCompanion( |     return ChatMessagesCompanion( | ||||||
|       id: Value(message.id), |       id: Value(message.id), | ||||||
|       roomId: Value(message.roomId), |       roomId: Value(message.roomId), | ||||||
|       senderId: Value(message.senderId), |       senderId: Value(message.senderId), | ||||||
|       content: Value(message.toRemoteMessage().content), |       content: Value(remote.content), | ||||||
|       nonce: Value(message.nonce), |       nonce: Value(message.nonce), | ||||||
|       data: Value(jsonEncode(message.data)), |       data: Value(jsonEncode(message.data)), | ||||||
|       createdAt: Value(message.createdAt), |       createdAt: Value(message.createdAt), | ||||||
|       status: Value(message.status), |       status: Value(message.status), | ||||||
|       isRead: Value(message.isRead), |       updatedAt: Value(remote.updatedAt), | ||||||
|  |       deletedAt: Value(remote.deletedAt), | ||||||
|  |       type: Value(remote.type), | ||||||
|  |       meta: Value(remote.meta), | ||||||
|  |       membersMentioned: Value(remote.membersMentioned), | ||||||
|  |       editedAt: Value(remote.editedAt), | ||||||
|  |       attachments: Value(remote.attachments.map((e) => e.toJson()).toList()), | ||||||
|  |       reactions: Value(remote.reactions.map((e) => e.toJson()).toList()), | ||||||
|  |       repliedMessageId: Value(remote.repliedMessageId), | ||||||
|  |       forwardedMessageId: Value(remote.forwardedMessageId), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -177,7 +216,18 @@ class AppDatabase extends _$AppDatabase { | |||||||
|       createdAt: dbMessage.createdAt, |       createdAt: dbMessage.createdAt, | ||||||
|       status: dbMessage.status, |       status: dbMessage.status, | ||||||
|       nonce: dbMessage.nonce, |       nonce: dbMessage.nonce, | ||||||
|       isRead: dbMessage.isRead, |       content: dbMessage.content, | ||||||
|  |       isDeleted: dbMessage.isDeleted, | ||||||
|  |       updatedAt: dbMessage.updatedAt, | ||||||
|  |       deletedAt: dbMessage.deletedAt, | ||||||
|  |       type: dbMessage.type, | ||||||
|  |       meta: dbMessage.meta, | ||||||
|  |       membersMentioned: dbMessage.membersMentioned, | ||||||
|  |       editedAt: dbMessage.editedAt, | ||||||
|  |       attachments: dbMessage.attachments, | ||||||
|  |       reactions: dbMessage.reactions, | ||||||
|  |       repliedMessageId: dbMessage.repliedMessageId, | ||||||
|  |       forwardedMessageId: dbMessage.forwardedMessageId, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,7 +1,41 @@ | |||||||
|  | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:drift/drift.dart'; | import 'package:drift/drift.dart'; | ||||||
| import 'package:island/models/chat.dart'; | import 'package:island/models/chat.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
|  |  | ||||||
|  | class MapConverter extends TypeConverter<Map<String, dynamic>, String> { | ||||||
|  |   const MapConverter(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Map<String, dynamic> fromSql(String fromDb) => json.decode(fromDb); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toSql(Map<String, dynamic> value) => json.encode(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class ListStringConverter extends TypeConverter<List<String>, String> { | ||||||
|  |   const ListStringConverter(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   List<String> fromSql(String fromDb) => List<String>.from(json.decode(fromDb)); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toSql(List<String> value) => json.encode(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class ListMapConverter | ||||||
|  |     extends TypeConverter<List<Map<String, dynamic>>, String> { | ||||||
|  |   const ListMapConverter(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   List<Map<String, dynamic>> fromSql(String fromDb) => | ||||||
|  |       List<Map<String, dynamic>>.from(json.decode(fromDb)); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toSql(List<Map<String, dynamic>> value) => json.encode(value); | ||||||
|  | } | ||||||
|  |  | ||||||
| class ChatMessages extends Table { | class ChatMessages extends Table { | ||||||
|   TextColumn get id => text()(); |   TextColumn get id => text()(); | ||||||
|   TextColumn get roomId => text()(); |   TextColumn get roomId => text()(); | ||||||
| @@ -11,7 +45,24 @@ class ChatMessages extends Table { | |||||||
|   TextColumn get data => text()(); |   TextColumn get data => text()(); | ||||||
|   DateTimeColumn get createdAt => dateTime()(); |   DateTimeColumn get createdAt => dateTime()(); | ||||||
|   IntColumn get status => intEnum<MessageStatus>()(); |   IntColumn get status => intEnum<MessageStatus>()(); | ||||||
|   BoolColumn get isRead => boolean().withDefault(const Constant(false))(); |   BoolColumn get isDeleted => | ||||||
|  |       boolean().nullable().withDefault(const Constant(false))(); | ||||||
|  |   DateTimeColumn get updatedAt => dateTime().nullable()(); | ||||||
|  |   DateTimeColumn get deletedAt => dateTime().nullable()(); | ||||||
|  |   TextColumn get type => text().withDefault(const Constant('text'))(); | ||||||
|  |   TextColumn get meta => | ||||||
|  |       text().map(const MapConverter()).withDefault(const Constant('{}'))(); | ||||||
|  |   TextColumn get membersMentioned => | ||||||
|  |       text() | ||||||
|  |           .map(const ListStringConverter()) | ||||||
|  |           .withDefault(const Constant('[]'))(); | ||||||
|  |   DateTimeColumn get editedAt => dateTime().nullable()(); | ||||||
|  |   TextColumn get attachments => | ||||||
|  |       text().map(const ListMapConverter()).withDefault(const Constant('[]'))(); | ||||||
|  |   TextColumn get reactions => | ||||||
|  |       text().map(const ListMapConverter()).withDefault(const Constant('[]'))(); | ||||||
|  |   TextColumn get repliedMessageId => text().nullable()(); | ||||||
|  |   TextColumn get forwardedMessageId => text().nullable()(); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Set<Column> get primaryKey => {id}; |   Set<Column> get primaryKey => {id}; | ||||||
| @@ -25,8 +76,19 @@ class LocalChatMessage { | |||||||
|   final DateTime createdAt; |   final DateTime createdAt; | ||||||
|   MessageStatus status; |   MessageStatus status; | ||||||
|   final String? nonce; |   final String? nonce; | ||||||
|  |   final String? content; | ||||||
|  |   final bool? isDeleted; | ||||||
|  |   final DateTime? updatedAt; | ||||||
|  |   final DateTime? deletedAt; | ||||||
|  |   final String type; | ||||||
|  |   final Map<String, dynamic> meta; | ||||||
|  |   final List<String> membersMentioned; | ||||||
|  |   final DateTime? editedAt; | ||||||
|  |   final List<Map<String, dynamic>> attachments; | ||||||
|  |   final List<Map<String, dynamic>> reactions; | ||||||
|  |   final String? repliedMessageId; | ||||||
|  |   final String? forwardedMessageId; | ||||||
|   List<UniversalFile>? localAttachments; |   List<UniversalFile>? localAttachments; | ||||||
|   bool isRead; |  | ||||||
|  |  | ||||||
|   LocalChatMessage({ |   LocalChatMessage({ | ||||||
|     required this.id, |     required this.id, | ||||||
| @@ -36,8 +98,19 @@ class LocalChatMessage { | |||||||
|     required this.createdAt, |     required this.createdAt, | ||||||
|     required this.nonce, |     required this.nonce, | ||||||
|     required this.status, |     required this.status, | ||||||
|  |     this.content, | ||||||
|  |     this.isDeleted, | ||||||
|  |     this.updatedAt, | ||||||
|  |     this.deletedAt, | ||||||
|  |     required this.type, | ||||||
|  |     required this.meta, | ||||||
|  |     required this.membersMentioned, | ||||||
|  |     this.editedAt, | ||||||
|  |     required this.attachments, | ||||||
|  |     required this.reactions, | ||||||
|  |     this.repliedMessageId, | ||||||
|  |     this.forwardedMessageId, | ||||||
|     this.localAttachments, |     this.localAttachments, | ||||||
|     this.isRead = false, |  | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   SnChatMessage toRemoteMessage() { |   SnChatMessage toRemoteMessage() { | ||||||
| @@ -48,7 +121,6 @@ class LocalChatMessage { | |||||||
|     SnChatMessage message, |     SnChatMessage message, | ||||||
|     MessageStatus status, { |     MessageStatus status, { | ||||||
|     String? nonce, |     String? nonce, | ||||||
|     bool isRead = false, |  | ||||||
|   }) { |   }) { | ||||||
|     return LocalChatMessage( |     return LocalChatMessage( | ||||||
|       id: message.id, |       id: message.id, | ||||||
| @@ -58,7 +130,18 @@ class LocalChatMessage { | |||||||
|       createdAt: message.createdAt, |       createdAt: message.createdAt, | ||||||
|       status: status, |       status: status, | ||||||
|       nonce: nonce ?? message.nonce, |       nonce: nonce ?? message.nonce, | ||||||
|       isRead: isRead, |       content: message.content, | ||||||
|  |       isDeleted: false, | ||||||
|  |       updatedAt: message.updatedAt, | ||||||
|  |       deletedAt: null, | ||||||
|  |       type: message.type, | ||||||
|  |       meta: message.meta, | ||||||
|  |       membersMentioned: message.membersMentioned, | ||||||
|  |       editedAt: message.editedAt, | ||||||
|  |       attachments: message.attachments.map((e) => e.toJson()).toList(), | ||||||
|  |       reactions: message.reactions.map((e) => e.toJson()).toList(), | ||||||
|  |       repliedMessageId: message.repliedMessageId, | ||||||
|  |       forwardedMessageId: message.forwardedMessageId, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -181,7 +181,7 @@ class IslandApp extends HookConsumerWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|       if (!kIsWeb && Platform.isLinux) { |       if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) { | ||||||
|         return null; |         return null; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -98,6 +98,7 @@ sealed class SnAccountStatus with _$SnAccountStatus { | |||||||
|     required bool isNotDisturb, |     required bool isNotDisturb, | ||||||
|     required bool isCustomized, |     required bool isCustomized, | ||||||
|     @Default("") String label, |     @Default("") String label, | ||||||
|  |     required Map<String, dynamic>? meta, | ||||||
|     required DateTime? clearedAt, |     required DateTime? clearedAt, | ||||||
|     required String accountId, |     required String accountId, | ||||||
|     required DateTime createdAt, |     required DateTime createdAt, | ||||||
|   | |||||||
| @@ -1053,7 +1053,7 @@ $SnVerificationMarkCopyWith<$Res>? get verification { | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAccountStatus { | mixin _$SnAccountStatus { | ||||||
|  |  | ||||||
|  String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; Map<String, dynamic>? get meta; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnAccountStatus | /// Create a copy of SnAccountStatus | ||||||
| /// 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) | ||||||
| @@ -1066,16 +1066,16 @@ $SnAccountStatusCopyWith<SnAccountStatus> get copyWith => _$SnAccountStatusCopyW | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(meta),clearedAt,accountId,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1086,7 +1086,7 @@ abstract mixin class $SnAccountStatusCopyWith<$Res>  { | |||||||
|   factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl; |   factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1103,7 +1103,7 @@ class _$SnAccountStatusCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAccountStatus | /// Create a copy of SnAccountStatus | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1112,7 +1112,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig | |||||||
| as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||||
| as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable | as String,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1199,10 +1200,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  Map<String, dynamic>? meta,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccountStatus() when $default != null: | case _SnAccountStatus() when $default != null: | ||||||
| return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -1220,10 +1221,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  Map<String, dynamic>? meta,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccountStatus(): | case _SnAccountStatus(): | ||||||
| return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -1237,10 +1238,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  Map<String, dynamic>? meta,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccountStatus() when $default != null: | case _SnAccountStatus() when $default != null: | ||||||
| return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -1252,7 +1253,7 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnAccountStatus implements SnAccountStatus { | class _SnAccountStatus implements SnAccountStatus { | ||||||
|   const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}); |   const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required final  Map<String, dynamic>? meta, required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta; | ||||||
|   factory _SnAccountStatus.fromJson(Map<String, dynamic> json) => _$SnAccountStatusFromJson(json); |   factory _SnAccountStatus.fromJson(Map<String, dynamic> json) => _$SnAccountStatusFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -1262,6 +1263,15 @@ class _SnAccountStatus implements SnAccountStatus { | |||||||
| @override final  bool isNotDisturb; | @override final  bool isNotDisturb; | ||||||
| @override final  bool isCustomized; | @override final  bool isCustomized; | ||||||
| @override@JsonKey() final  String label; | @override@JsonKey() final  String label; | ||||||
|  |  final  Map<String, dynamic>? _meta; | ||||||
|  | @override Map<String, dynamic>? get meta { | ||||||
|  |   final value = _meta; | ||||||
|  |   if (value == null) return null; | ||||||
|  |   if (_meta is EqualUnmodifiableMapView) return _meta; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableMapView(value); | ||||||
|  | } | ||||||
|  |  | ||||||
| @override final  DateTime? clearedAt; | @override final  DateTime? clearedAt; | ||||||
| @override final  String accountId; | @override final  String accountId; | ||||||
| @override final  DateTime createdAt; | @override final  DateTime createdAt; | ||||||
| @@ -1281,16 +1291,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(_meta),clearedAt,accountId,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1301,7 +1311,7 @@ abstract mixin class _$SnAccountStatusCopyWith<$Res> implements $SnAccountStatus | |||||||
|   factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl; |   factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1318,7 +1328,7 @@ class __$SnAccountStatusCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAccountStatus | /// Create a copy of SnAccountStatus | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnAccountStatus( |   return _then(_SnAccountStatus( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1327,7 +1337,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig | |||||||
| as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||||
| as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable | as String,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
|   | |||||||
| @@ -158,6 +158,7 @@ _SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) => | |||||||
|       isNotDisturb: json['is_not_disturb'] as bool, |       isNotDisturb: json['is_not_disturb'] as bool, | ||||||
|       isCustomized: json['is_customized'] as bool, |       isCustomized: json['is_customized'] as bool, | ||||||
|       label: json['label'] as String? ?? "", |       label: json['label'] as String? ?? "", | ||||||
|  |       meta: json['meta'] as Map<String, dynamic>?, | ||||||
|       clearedAt: |       clearedAt: | ||||||
|           json['cleared_at'] == null |           json['cleared_at'] == null | ||||||
|               ? null |               ? null | ||||||
| @@ -180,6 +181,7 @@ Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) => | |||||||
|       'is_not_disturb': instance.isNotDisturb, |       'is_not_disturb': instance.isNotDisturb, | ||||||
|       'is_customized': instance.isCustomized, |       'is_customized': instance.isCustomized, | ||||||
|       'label': instance.label, |       'label': instance.label, | ||||||
|  |       'meta': instance.meta, | ||||||
|       'cleared_at': instance.clearedAt?.toIso8601String(), |       'cleared_at': instance.clearedAt?.toIso8601String(), | ||||||
|       'account_id': instance.accountId, |       'account_id': instance.accountId, | ||||||
|       'created_at': instance.createdAt.toIso8601String(), |       'created_at': instance.createdAt.toIso8601String(), | ||||||
|   | |||||||
| @@ -14,11 +14,11 @@ sealed class AppToken with _$AppToken { | |||||||
| @freezed | @freezed | ||||||
| sealed class GeoIpLocation with _$GeoIpLocation { | sealed class GeoIpLocation with _$GeoIpLocation { | ||||||
|   const factory GeoIpLocation({ |   const factory GeoIpLocation({ | ||||||
|     required double latitude, |     required double? latitude, | ||||||
|     required double longitude, |     required double? longitude, | ||||||
|     required String countryCode, |     required String? countryCode, | ||||||
|     required String country, |     required String? country, | ||||||
|     required String city, |     required String? city, | ||||||
|   }) = _GeoIpLocation; |   }) = _GeoIpLocation; | ||||||
|  |  | ||||||
|   factory GeoIpLocation.fromJson(Map<String, dynamic> json) => |   factory GeoIpLocation.fromJson(Map<String, dynamic> json) => | ||||||
| @@ -29,7 +29,7 @@ sealed class GeoIpLocation with _$GeoIpLocation { | |||||||
| sealed class SnAuthChallenge with _$SnAuthChallenge { | sealed class SnAuthChallenge with _$SnAuthChallenge { | ||||||
|   const factory SnAuthChallenge({ |   const factory SnAuthChallenge({ | ||||||
|     required String id, |     required String id, | ||||||
|     required DateTime expiredAt, |     required DateTime? expiredAt, | ||||||
|     required int stepRemain, |     required int stepRemain, | ||||||
|     required int stepTotal, |     required int stepTotal, | ||||||
|     required int failedAttempts, |     required int failedAttempts, | ||||||
| @@ -57,7 +57,7 @@ sealed class SnAuthSession with _$SnAuthSession { | |||||||
|     required String id, |     required String id, | ||||||
|     required String? label, |     required String? label, | ||||||
|     required DateTime lastGrantedAt, |     required DateTime lastGrantedAt, | ||||||
|     required DateTime expiredAt, |     required DateTime? expiredAt, | ||||||
|     required String accountId, |     required String accountId, | ||||||
|     required String challengeId, |     required String challengeId, | ||||||
|     required SnAuthChallenge challenge, |     required SnAuthChallenge challenge, | ||||||
|   | |||||||
| @@ -272,7 +272,7 @@ as String, | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$GeoIpLocation { | mixin _$GeoIpLocation { | ||||||
|  |  | ||||||
|  double get latitude; double get longitude; String get countryCode; String get country; String get city; |  double? get latitude; double? get longitude; String? get countryCode; String? get country; String? get city; | ||||||
| /// Create a copy of GeoIpLocation | /// Create a copy of GeoIpLocation | ||||||
| /// 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) | ||||||
| @@ -305,7 +305,7 @@ abstract mixin class $GeoIpLocationCopyWith<$Res>  { | |||||||
|   factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl; |   factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  double latitude, double longitude, String countryCode, String country, String city |  double? latitude, double? longitude, String? countryCode, String? country, String? city | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -322,14 +322,14 @@ class _$GeoIpLocationCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of GeoIpLocation | /// Create a copy of GeoIpLocation | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | ||||||
| as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | ||||||
| as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||||
| as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | ||||||
| as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | ||||||
| as String, | as String?, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -411,7 +411,7 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double? latitude,  double? longitude,  String? countryCode,  String? country,  String? city)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _GeoIpLocation() when $default != null: | case _GeoIpLocation() when $default != null: | ||||||
| return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: | return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: | ||||||
| @@ -432,7 +432,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_ | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double latitude,  double longitude,  String countryCode,  String country,  String city)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double? latitude,  double? longitude,  String? countryCode,  String? country,  String? city)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _GeoIpLocation(): | case _GeoIpLocation(): | ||||||
| return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);} | return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);} | ||||||
| @@ -449,7 +449,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_ | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double? latitude,  double? longitude,  String? countryCode,  String? country,  String? city)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _GeoIpLocation() when $default != null: | case _GeoIpLocation() when $default != null: | ||||||
| return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: | return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: | ||||||
| @@ -467,11 +467,11 @@ class _GeoIpLocation implements GeoIpLocation { | |||||||
|   const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city}); |   const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city}); | ||||||
|   factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json); |   factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json); | ||||||
|  |  | ||||||
| @override final  double latitude; | @override final  double? latitude; | ||||||
| @override final  double longitude; | @override final  double? longitude; | ||||||
| @override final  String countryCode; | @override final  String? countryCode; | ||||||
| @override final  String country; | @override final  String? country; | ||||||
| @override final  String city; | @override final  String? city; | ||||||
|  |  | ||||||
| /// Create a copy of GeoIpLocation | /// Create a copy of GeoIpLocation | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @@ -506,7 +506,7 @@ abstract mixin class _$GeoIpLocationCopyWith<$Res> implements $GeoIpLocationCopy | |||||||
|   factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl; |   factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  double latitude, double longitude, String countryCode, String country, String city |  double? latitude, double? longitude, String? countryCode, String? country, String? city | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -523,14 +523,14 @@ class __$GeoIpLocationCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of GeoIpLocation | /// Create a copy of GeoIpLocation | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) { | ||||||
|   return _then(_GeoIpLocation( |   return _then(_GeoIpLocation( | ||||||
| latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | ||||||
| as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | ||||||
| as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||||
| as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | ||||||
| as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | ||||||
| as String, | as String?, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -541,7 +541,7 @@ as String, | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAuthChallenge { | mixin _$SnAuthChallenge { | ||||||
|  |  | ||||||
|  String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; DateTime? get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnAuthChallenge | /// Create a copy of SnAuthChallenge | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -574,7 +574,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res>  { | |||||||
|   factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; |   factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -591,11 +591,11 @@ class _$SnAuthChallengeCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAuthChallenge | /// Create a copy of SnAuthChallenge | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | ||||||
| as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | ||||||
| as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | ||||||
| as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -704,7 +704,7 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime? expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge() when $default != null: | case _SnAuthChallenge() when $default != null: | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
| @@ -725,7 +725,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that. | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime? expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge(): | case _SnAuthChallenge(): | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| @@ -742,7 +742,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that. | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime? expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge() when $default != null: | case _SnAuthChallenge() when $default != null: | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
| @@ -761,7 +761,7 @@ class _SnAuthChallenge implements SnAuthChallenge { | |||||||
|   factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json); |   factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @override final  DateTime expiredAt; | @override final  DateTime? expiredAt; | ||||||
| @override final  int stepRemain; | @override final  int stepRemain; | ||||||
| @override final  int stepTotal; | @override final  int stepTotal; | ||||||
| @override final  int failedAttempts; | @override final  int failedAttempts; | ||||||
| @@ -829,7 +829,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge | |||||||
|   factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; |   factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -846,11 +846,11 @@ class __$SnAuthChallengeCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAuthChallenge | /// Create a copy of SnAuthChallenge | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnAuthChallenge( |   return _then(_SnAuthChallenge( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | ||||||
| as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | ||||||
| as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | ||||||
| as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -888,7 +888,7 @@ $GeoIpLocationCopyWith<$Res>? get location { | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAuthSession { | mixin _$SnAuthSession { | ||||||
|  |  | ||||||
|  String get id; String? get label; DateTime get lastGrantedAt; DateTime get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; String? get label; DateTime get lastGrantedAt; DateTime? get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnAuthSession | /// Create a copy of SnAuthSession | ||||||
| /// 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) | ||||||
| @@ -921,7 +921,7 @@ abstract mixin class $SnAuthSessionCopyWith<$Res>  { | |||||||
|   factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl; |   factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -938,13 +938,13 @@ class _$SnAuthSessionCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAuthSession | /// Create a copy of SnAuthSession | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable | as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable | as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable | as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable | ||||||
| as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1041,7 +1041,7 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime? expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthSession() when $default != null: | case _SnAuthSession() when $default != null: | ||||||
| return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
| @@ -1062,7 +1062,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime? expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthSession(): | case _SnAuthSession(): | ||||||
| return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| @@ -1079,7 +1079,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime? expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthSession() when $default != null: | case _SnAuthSession() when $default != null: | ||||||
| return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
| @@ -1100,7 +1100,7 @@ class _SnAuthSession implements SnAuthSession { | |||||||
| @override final  String id; | @override final  String id; | ||||||
| @override final  String? label; | @override final  String? label; | ||||||
| @override final  DateTime lastGrantedAt; | @override final  DateTime lastGrantedAt; | ||||||
| @override final  DateTime expiredAt; | @override final  DateTime? expiredAt; | ||||||
| @override final  String accountId; | @override final  String accountId; | ||||||
| @override final  String challengeId; | @override final  String challengeId; | ||||||
| @override final  SnAuthChallenge challenge; | @override final  SnAuthChallenge challenge; | ||||||
| @@ -1141,7 +1141,7 @@ abstract mixin class _$SnAuthSessionCopyWith<$Res> implements $SnAuthSessionCopy | |||||||
|   factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl; |   factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1158,13 +1158,13 @@ class __$SnAuthSessionCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAuthSession | /// Create a copy of SnAuthSession | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnAuthSession( |   return _then(_SnAuthSession( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable | as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable | as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable | as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable | ||||||
| as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
|   | |||||||
| @@ -15,11 +15,11 @@ Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{ | |||||||
|  |  | ||||||
| _GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) => | _GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) => | ||||||
|     _GeoIpLocation( |     _GeoIpLocation( | ||||||
|       latitude: (json['latitude'] as num).toDouble(), |       latitude: (json['latitude'] as num?)?.toDouble(), | ||||||
|       longitude: (json['longitude'] as num).toDouble(), |       longitude: (json['longitude'] as num?)?.toDouble(), | ||||||
|       countryCode: json['country_code'] as String, |       countryCode: json['country_code'] as String?, | ||||||
|       country: json['country'] as String, |       country: json['country'] as String?, | ||||||
|       city: json['city'] as String, |       city: json['city'] as String?, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) => | Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) => | ||||||
| @@ -34,7 +34,10 @@ Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) => | |||||||
| _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | ||||||
|     _SnAuthChallenge( |     _SnAuthChallenge( | ||||||
|       id: json['id'] as String, |       id: json['id'] as String, | ||||||
|       expiredAt: DateTime.parse(json['expired_at'] as String), |       expiredAt: | ||||||
|  |           json['expired_at'] == null | ||||||
|  |               ? null | ||||||
|  |               : DateTime.parse(json['expired_at'] as String), | ||||||
|       stepRemain: (json['step_remain'] as num).toInt(), |       stepRemain: (json['step_remain'] as num).toInt(), | ||||||
|       stepTotal: (json['step_total'] as num).toInt(), |       stepTotal: (json['step_total'] as num).toInt(), | ||||||
|       failedAttempts: (json['failed_attempts'] as num).toInt(), |       failedAttempts: (json['failed_attempts'] as num).toInt(), | ||||||
| @@ -66,7 +69,7 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | |||||||
| Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) => | Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) => | ||||||
|     <String, dynamic>{ |     <String, dynamic>{ | ||||||
|       'id': instance.id, |       'id': instance.id, | ||||||
|       'expired_at': instance.expiredAt.toIso8601String(), |       'expired_at': instance.expiredAt?.toIso8601String(), | ||||||
|       'step_remain': instance.stepRemain, |       'step_remain': instance.stepRemain, | ||||||
|       'step_total': instance.stepTotal, |       'step_total': instance.stepTotal, | ||||||
|       'failed_attempts': instance.failedAttempts, |       'failed_attempts': instance.failedAttempts, | ||||||
| @@ -89,7 +92,10 @@ _SnAuthSession _$SnAuthSessionFromJson(Map<String, dynamic> json) => | |||||||
|       id: json['id'] as String, |       id: json['id'] as String, | ||||||
|       label: json['label'] as String?, |       label: json['label'] as String?, | ||||||
|       lastGrantedAt: DateTime.parse(json['last_granted_at'] as String), |       lastGrantedAt: DateTime.parse(json['last_granted_at'] as String), | ||||||
|       expiredAt: DateTime.parse(json['expired_at'] as String), |       expiredAt: | ||||||
|  |           json['expired_at'] == null | ||||||
|  |               ? null | ||||||
|  |               : DateTime.parse(json['expired_at'] as String), | ||||||
|       accountId: json['account_id'] as String, |       accountId: json['account_id'] as String, | ||||||
|       challengeId: json['challenge_id'] as String, |       challengeId: json['challenge_id'] as String, | ||||||
|       challenge: SnAuthChallenge.fromJson( |       challenge: SnAuthChallenge.fromJson( | ||||||
| @@ -108,7 +114,7 @@ Map<String, dynamic> _$SnAuthSessionToJson(_SnAuthSession instance) => | |||||||
|       'id': instance.id, |       'id': instance.id, | ||||||
|       'label': instance.label, |       'label': instance.label, | ||||||
|       'last_granted_at': instance.lastGrantedAt.toIso8601String(), |       'last_granted_at': instance.lastGrantedAt.toIso8601String(), | ||||||
|       'expired_at': instance.expiredAt.toIso8601String(), |       'expired_at': instance.expiredAt?.toIso8601String(), | ||||||
|       'account_id': instance.accountId, |       'account_id': instance.accountId, | ||||||
|       'challenge_id': instance.challengeId, |       'challenge_id': instance.challengeId, | ||||||
|       'challenge': instance.challenge.toJson(), |       'challenge': instance.challenge.toJson(), | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ sealed class SnChatMessage with _$SnChatMessage { | |||||||
|     String? content, |     String? content, | ||||||
|     String? nonce, |     String? nonce, | ||||||
|     @Default({}) Map<String, dynamic> meta, |     @Default({}) Map<String, dynamic> meta, | ||||||
|     @Default([]) List<String> membersMetioned, |     @Default([]) List<String> membersMentioned, | ||||||
|     DateTime? editedAt, |     DateTime? editedAt, | ||||||
|     @Default([]) List<SnCloudFile> attachments, |     @Default([]) List<SnCloudFile> attachments, | ||||||
|     @Default([]) List<SnChatReaction> reactions, |     @Default([]) List<SnChatReaction> reactions, | ||||||
| @@ -117,23 +117,10 @@ class MessageChangeAction { | |||||||
|   static const String delete = "delete"; |   static const String delete = "delete"; | ||||||
| } | } | ||||||
|  |  | ||||||
| @freezed |  | ||||||
| sealed class MessageChange with _$MessageChange { |  | ||||||
|   const factory MessageChange({ |  | ||||||
|     required String messageId, |  | ||||||
|     required String action, |  | ||||||
|     SnChatMessage? message, |  | ||||||
|     required DateTime timestamp, |  | ||||||
|   }) = _MessageChange; |  | ||||||
|  |  | ||||||
|   factory MessageChange.fromJson(Map<String, dynamic> json) => |  | ||||||
|       _$MessageChangeFromJson(json); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @freezed | @freezed | ||||||
| sealed class MessageSyncResponse with _$MessageSyncResponse { | sealed class MessageSyncResponse with _$MessageSyncResponse { | ||||||
|   const factory MessageSyncResponse({ |   const factory MessageSyncResponse({ | ||||||
|     @Default([]) List<MessageChange> changes, |     @Default([]) List<SnChatMessage> messages, | ||||||
|     required DateTime currentTimestamp, |     required DateTime currentTimestamp, | ||||||
|   }) = _MessageSyncResponse; |   }) = _MessageSyncResponse; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -391,7 +391,7 @@ $SnRealmCopyWith<$Res>? get realm { | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnChatMessage { | mixin _$SnChatMessage { | ||||||
|  |  | ||||||
|  DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId; |  DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMentioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId; | ||||||
| /// Create a copy of SnChatMessage | /// Create a copy of SnChatMessage | ||||||
| /// 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) | ||||||
| @@ -404,16 +404,16 @@ $SnChatMessageCopyWith<SnChatMessage> get copyWith => _$SnChatMessageCopyWithImp | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMentioned, membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); | int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMentioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; |   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -424,7 +424,7 @@ abstract mixin class $SnChatMessageCopyWith<$Res>  { | |||||||
|   factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl; |   factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId |  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -441,7 +441,7 @@ class _$SnChatMessageCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnChatMessage | /// Create a copy of SnChatMessage | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -451,7 +451,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non | |||||||
| as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable | as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self.membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self.membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable | ||||||
| as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable | as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable | as DateTime?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnCloudFile>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable | as List<SnCloudFile>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -551,10 +551,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  String id,  String type,  String? content,  String? nonce,  Map<String, dynamic> meta,  List<String> membersMetioned,  DateTime? editedAt,  List<SnCloudFile> attachments,  List<SnChatReaction> reactions,  String? repliedMessageId,  String? forwardedMessageId,  String senderId,  SnChatMember sender,  String chatRoomId)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  String id,  String type,  String? content,  String? nonce,  Map<String, dynamic> meta,  List<String> membersMentioned,  DateTime? editedAt,  List<SnCloudFile> attachments,  List<SnChatReaction> reactions,  String? repliedMessageId,  String? forwardedMessageId,  String senderId,  SnChatMember sender,  String chatRoomId)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnChatMessage() when $default != null: | case _SnChatMessage() when $default != null: | ||||||
| return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _: | return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -572,10 +572,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  String id,  String type,  String? content,  String? nonce,  Map<String, dynamic> meta,  List<String> membersMetioned,  DateTime? editedAt,  List<SnCloudFile> attachments,  List<SnChatReaction> reactions,  String? repliedMessageId,  String? forwardedMessageId,  String senderId,  SnChatMember sender,  String chatRoomId)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  String id,  String type,  String? content,  String? nonce,  Map<String, dynamic> meta,  List<String> membersMentioned,  DateTime? editedAt,  List<SnCloudFile> attachments,  List<SnChatReaction> reactions,  String? repliedMessageId,  String? forwardedMessageId,  String senderId,  SnChatMember sender,  String chatRoomId)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnChatMessage(): | case _SnChatMessage(): | ||||||
| return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);} | return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -589,10 +589,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  String id,  String type,  String? content,  String? nonce,  Map<String, dynamic> meta,  List<String> membersMetioned,  DateTime? editedAt,  List<SnCloudFile> attachments,  List<SnChatReaction> reactions,  String? repliedMessageId,  String? forwardedMessageId,  String senderId,  SnChatMember sender,  String chatRoomId)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  String id,  String type,  String? content,  String? nonce,  Map<String, dynamic> meta,  List<String> membersMentioned,  DateTime? editedAt,  List<SnCloudFile> attachments,  List<SnChatReaction> reactions,  String? repliedMessageId,  String? forwardedMessageId,  String senderId,  SnChatMember sender,  String chatRoomId)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnChatMessage() when $default != null: | case _SnChatMessage() when $default != null: | ||||||
| return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _: | return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -604,7 +604,7 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnChatMessage implements SnChatMessage { | class _SnChatMessage implements SnChatMessage { | ||||||
|   const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final  Map<String, dynamic> meta = const {}, final  List<String> membersMetioned = const [], this.editedAt, final  List<SnCloudFile> attachments = const [], final  List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions; |   const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final  Map<String, dynamic> meta = const {}, final  List<String> membersMentioned = const [], this.editedAt, final  List<SnCloudFile> attachments = const [], final  List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMentioned = membersMentioned,_attachments = attachments,_reactions = reactions; | ||||||
|   factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json); |   factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json); | ||||||
|  |  | ||||||
| @override final  DateTime createdAt; | @override final  DateTime createdAt; | ||||||
| @@ -621,11 +621,11 @@ class _SnChatMessage implements SnChatMessage { | |||||||
|   return EqualUnmodifiableMapView(_meta); |   return EqualUnmodifiableMapView(_meta); | ||||||
| } | } | ||||||
|  |  | ||||||
|  final  List<String> _membersMetioned; |  final  List<String> _membersMentioned; | ||||||
| @override@JsonKey() List<String> get membersMetioned { | @override@JsonKey() List<String> get membersMentioned { | ||||||
|   if (_membersMetioned is EqualUnmodifiableListView) return _membersMetioned; |   if (_membersMentioned is EqualUnmodifiableListView) return _membersMentioned; | ||||||
|   // ignore: implicit_dynamic_type |   // ignore: implicit_dynamic_type | ||||||
|   return EqualUnmodifiableListView(_membersMetioned); |   return EqualUnmodifiableListView(_membersMentioned); | ||||||
| } | } | ||||||
|  |  | ||||||
| @override final  DateTime? editedAt; | @override final  DateTime? editedAt; | ||||||
| @@ -662,16 +662,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMentioned, _membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); | int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMentioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; |   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -682,7 +682,7 @@ abstract mixin class _$SnChatMessageCopyWith<$Res> implements $SnChatMessageCopy | |||||||
|   factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl; |   factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId |  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -699,7 +699,7 @@ class __$SnChatMessageCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnChatMessage | /// Create a copy of SnChatMessage | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | ||||||
|   return _then(_SnChatMessage( |   return _then(_SnChatMessage( | ||||||
| createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -709,7 +709,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non | |||||||
| as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable | as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self._membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self._membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable | ||||||
| as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable | as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable | as DateTime?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnCloudFile>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable | as List<SnCloudFile>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1691,300 +1691,10 @@ $SnChatMessageCopyWith<$Res>? get lastMessage { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| mixin _$MessageChange { |  | ||||||
|  |  | ||||||
|  String get messageId; String get action; SnChatMessage? get message; DateTime get timestamp; |  | ||||||
| /// Create a copy of MessageChange |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @pragma('vm:prefer-inline') |  | ||||||
| $MessageChangeCopyWith<MessageChange> get copyWith => _$MessageChangeCopyWithImpl<MessageChange>(this as MessageChange, _$identity); |  | ||||||
|  |  | ||||||
|   /// Serializes this MessageChange to a JSON map. |  | ||||||
|   Map<String, dynamic> toJson(); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| bool operator ==(Object other) { |  | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @override |  | ||||||
| int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp); |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| String toString() { |  | ||||||
|   return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)'; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| abstract mixin class $MessageChangeCopyWith<$Res>  { |  | ||||||
|   factory $MessageChangeCopyWith(MessageChange value, $Res Function(MessageChange) _then) = _$MessageChangeCopyWithImpl; |  | ||||||
| @useResult |  | ||||||
| $Res call({ |  | ||||||
|  String messageId, String action, SnChatMessage? message, DateTime timestamp |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| $SnChatMessageCopyWith<$Res>? get message; |  | ||||||
|  |  | ||||||
| } |  | ||||||
| /// @nodoc |  | ||||||
| class _$MessageChangeCopyWithImpl<$Res> |  | ||||||
|     implements $MessageChangeCopyWith<$Res> { |  | ||||||
|   _$MessageChangeCopyWithImpl(this._self, this._then); |  | ||||||
|  |  | ||||||
|   final MessageChange _self; |  | ||||||
|   final $Res Function(MessageChange) _then; |  | ||||||
|  |  | ||||||
| /// Create a copy of MessageChange |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) { |  | ||||||
|   return _then(_self.copyWith( |  | ||||||
| messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable |  | ||||||
| as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable |  | ||||||
| as DateTime, |  | ||||||
|   )); |  | ||||||
| } |  | ||||||
| /// Create a copy of MessageChange |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @override |  | ||||||
| @pragma('vm:prefer-inline') |  | ||||||
| $SnChatMessageCopyWith<$Res>? get message { |  | ||||||
|     if (_self.message == null) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return $SnChatMessageCopyWith<$Res>(_self.message!, (value) { |  | ||||||
|     return _then(_self.copyWith(message: value)); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /// Adds pattern-matching-related methods to [MessageChange]. |  | ||||||
| extension MessageChangePatterns on MessageChange { |  | ||||||
| /// 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( _MessageChange value)?  $default,{required TResult orElse(),}){ |  | ||||||
| final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _MessageChange() 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( _MessageChange value)  $default,){ |  | ||||||
| final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _MessageChange(): |  | ||||||
| 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( _MessageChange value)?  $default,){ |  | ||||||
| final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _MessageChange() 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 messageId,  String action,  SnChatMessage? message,  DateTime timestamp)?  $default,{required TResult orElse(),}) {final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _MessageChange() when $default != null: |  | ||||||
| return $default(_that.messageId,_that.action,_that.message,_that.timestamp);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 messageId,  String action,  SnChatMessage? message,  DateTime timestamp)  $default,) {final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _MessageChange(): |  | ||||||
| return $default(_that.messageId,_that.action,_that.message,_that.timestamp);} |  | ||||||
| } |  | ||||||
| /// 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 messageId,  String action,  SnChatMessage? message,  DateTime timestamp)?  $default,) {final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _MessageChange() when $default != null: |  | ||||||
| return $default(_that.messageId,_that.action,_that.message,_that.timestamp);case _: |  | ||||||
|   return null; |  | ||||||
|  |  | ||||||
| } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| @JsonSerializable() |  | ||||||
|  |  | ||||||
| class _MessageChange implements MessageChange { |  | ||||||
|   const _MessageChange({required this.messageId, required this.action, this.message, required this.timestamp}); |  | ||||||
|   factory _MessageChange.fromJson(Map<String, dynamic> json) => _$MessageChangeFromJson(json); |  | ||||||
|  |  | ||||||
| @override final  String messageId; |  | ||||||
| @override final  String action; |  | ||||||
| @override final  SnChatMessage? message; |  | ||||||
| @override final  DateTime timestamp; |  | ||||||
|  |  | ||||||
| /// Create a copy of MessageChange |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @pragma('vm:prefer-inline') |  | ||||||
| _$MessageChangeCopyWith<_MessageChange> get copyWith => __$MessageChangeCopyWithImpl<_MessageChange>(this, _$identity); |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| Map<String, dynamic> toJson() { |  | ||||||
|   return _$MessageChangeToJson(this, ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| bool operator ==(Object other) { |  | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @override |  | ||||||
| int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp); |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| String toString() { |  | ||||||
|   return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)'; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| abstract mixin class _$MessageChangeCopyWith<$Res> implements $MessageChangeCopyWith<$Res> { |  | ||||||
|   factory _$MessageChangeCopyWith(_MessageChange value, $Res Function(_MessageChange) _then) = __$MessageChangeCopyWithImpl; |  | ||||||
| @override @useResult |  | ||||||
| $Res call({ |  | ||||||
|  String messageId, String action, SnChatMessage? message, DateTime timestamp |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @override $SnChatMessageCopyWith<$Res>? get message; |  | ||||||
|  |  | ||||||
| } |  | ||||||
| /// @nodoc |  | ||||||
| class __$MessageChangeCopyWithImpl<$Res> |  | ||||||
|     implements _$MessageChangeCopyWith<$Res> { |  | ||||||
|   __$MessageChangeCopyWithImpl(this._self, this._then); |  | ||||||
|  |  | ||||||
|   final _MessageChange _self; |  | ||||||
|   final $Res Function(_MessageChange) _then; |  | ||||||
|  |  | ||||||
| /// Create a copy of MessageChange |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) { |  | ||||||
|   return _then(_MessageChange( |  | ||||||
| messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable |  | ||||||
| as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable |  | ||||||
| as DateTime, |  | ||||||
|   )); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Create a copy of MessageChange |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @override |  | ||||||
| @pragma('vm:prefer-inline') |  | ||||||
| $SnChatMessageCopyWith<$Res>? get message { |  | ||||||
|     if (_self.message == null) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return $SnChatMessageCopyWith<$Res>(_self.message!, (value) { |  | ||||||
|     return _then(_self.copyWith(message: value)); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$MessageSyncResponse { | mixin _$MessageSyncResponse { | ||||||
|  |  | ||||||
|  List<MessageChange> get changes; DateTime get currentTimestamp; |  List<SnChatMessage> get messages; DateTime get currentTimestamp; | ||||||
| /// Create a copy of MessageSyncResponse | /// Create a copy of MessageSyncResponse | ||||||
| /// 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) | ||||||
| @@ -1997,16 +1707,16 @@ $MessageSyncResponseCopyWith<MessageSyncResponse> get copyWith => _$MessageSyncR | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.changes, changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.messages, messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(changes),currentTimestamp); | int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(messages),currentTimestamp); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)'; |   return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -2017,7 +1727,7 @@ abstract mixin class $MessageSyncResponseCopyWith<$Res>  { | |||||||
|   factory $MessageSyncResponseCopyWith(MessageSyncResponse value, $Res Function(MessageSyncResponse) _then) = _$MessageSyncResponseCopyWithImpl; |   factory $MessageSyncResponseCopyWith(MessageSyncResponse value, $Res Function(MessageSyncResponse) _then) = _$MessageSyncResponseCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  List<MessageChange> changes, DateTime currentTimestamp |  List<SnChatMessage> messages, DateTime currentTimestamp | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -2034,10 +1744,10 @@ class _$MessageSyncResponseCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of MessageSyncResponse | /// Create a copy of MessageSyncResponse | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? changes = null,Object? currentTimestamp = null,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? messages = null,Object? currentTimestamp = null,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| changes: null == changes ? _self.changes : changes // ignore: cast_nullable_to_non_nullable | messages: null == messages ? _self.messages : messages // ignore: cast_nullable_to_non_nullable | ||||||
| as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable | as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime, | as DateTime, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
| @@ -2120,10 +1830,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<MessageChange> changes,  DateTime currentTimestamp)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<SnChatMessage> messages,  DateTime currentTimestamp)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _MessageSyncResponse() when $default != null: | case _MessageSyncResponse() when $default != null: | ||||||
| return $default(_that.changes,_that.currentTimestamp);case _: | return $default(_that.messages,_that.currentTimestamp);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -2141,10 +1851,10 @@ return $default(_that.changes,_that.currentTimestamp);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<MessageChange> changes,  DateTime currentTimestamp)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<SnChatMessage> messages,  DateTime currentTimestamp)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _MessageSyncResponse(): | case _MessageSyncResponse(): | ||||||
| return $default(_that.changes,_that.currentTimestamp);} | return $default(_that.messages,_that.currentTimestamp);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -2158,10 +1868,10 @@ return $default(_that.changes,_that.currentTimestamp);} | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<MessageChange> changes,  DateTime currentTimestamp)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<SnChatMessage> messages,  DateTime currentTimestamp)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _MessageSyncResponse() when $default != null: | case _MessageSyncResponse() when $default != null: | ||||||
| return $default(_that.changes,_that.currentTimestamp);case _: | return $default(_that.messages,_that.currentTimestamp);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -2173,14 +1883,14 @@ return $default(_that.changes,_that.currentTimestamp);case _: | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _MessageSyncResponse implements MessageSyncResponse { | class _MessageSyncResponse implements MessageSyncResponse { | ||||||
|   const _MessageSyncResponse({final  List<MessageChange> changes = const [], required this.currentTimestamp}): _changes = changes; |   const _MessageSyncResponse({final  List<SnChatMessage> messages = const [], required this.currentTimestamp}): _messages = messages; | ||||||
|   factory _MessageSyncResponse.fromJson(Map<String, dynamic> json) => _$MessageSyncResponseFromJson(json); |   factory _MessageSyncResponse.fromJson(Map<String, dynamic> json) => _$MessageSyncResponseFromJson(json); | ||||||
|  |  | ||||||
|  final  List<MessageChange> _changes; |  final  List<SnChatMessage> _messages; | ||||||
| @override@JsonKey() List<MessageChange> get changes { | @override@JsonKey() List<SnChatMessage> get messages { | ||||||
|   if (_changes is EqualUnmodifiableListView) return _changes; |   if (_messages is EqualUnmodifiableListView) return _messages; | ||||||
|   // ignore: implicit_dynamic_type |   // ignore: implicit_dynamic_type | ||||||
|   return EqualUnmodifiableListView(_changes); |   return EqualUnmodifiableListView(_messages); | ||||||
| } | } | ||||||
|  |  | ||||||
| @override final  DateTime currentTimestamp; | @override final  DateTime currentTimestamp; | ||||||
| @@ -2198,16 +1908,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._changes, _changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._messages, _messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_changes),currentTimestamp); | int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_messages),currentTimestamp); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)'; |   return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -2218,7 +1928,7 @@ abstract mixin class _$MessageSyncResponseCopyWith<$Res> implements $MessageSync | |||||||
|   factory _$MessageSyncResponseCopyWith(_MessageSyncResponse value, $Res Function(_MessageSyncResponse) _then) = __$MessageSyncResponseCopyWithImpl; |   factory _$MessageSyncResponseCopyWith(_MessageSyncResponse value, $Res Function(_MessageSyncResponse) _then) = __$MessageSyncResponseCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  List<MessageChange> changes, DateTime currentTimestamp |  List<SnChatMessage> messages, DateTime currentTimestamp | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -2235,10 +1945,10 @@ class __$MessageSyncResponseCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of MessageSyncResponse | /// Create a copy of MessageSyncResponse | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? changes = null,Object? currentTimestamp = null,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? messages = null,Object? currentTimestamp = null,}) { | ||||||
|   return _then(_MessageSyncResponse( |   return _then(_MessageSyncResponse( | ||||||
| changes: null == changes ? _self._changes : changes // ignore: cast_nullable_to_non_nullable | messages: null == messages ? _self._messages : messages // ignore: cast_nullable_to_non_nullable | ||||||
| as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable | as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime, | as DateTime, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -69,8 +69,8 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) => | |||||||
|       content: json['content'] as String?, |       content: json['content'] as String?, | ||||||
|       nonce: json['nonce'] as String?, |       nonce: json['nonce'] as String?, | ||||||
|       meta: json['meta'] as Map<String, dynamic>? ?? const {}, |       meta: json['meta'] as Map<String, dynamic>? ?? const {}, | ||||||
|       membersMetioned: |       membersMentioned: | ||||||
|           (json['members_metioned'] as List<dynamic>?) |           (json['members_mentioned'] as List<dynamic>?) | ||||||
|               ?.map((e) => e as String) |               ?.map((e) => e as String) | ||||||
|               .toList() ?? |               .toList() ?? | ||||||
|           const [], |           const [], | ||||||
| @@ -105,7 +105,7 @@ Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) => | |||||||
|       'content': instance.content, |       'content': instance.content, | ||||||
|       'nonce': instance.nonce, |       'nonce': instance.nonce, | ||||||
|       'meta': instance.meta, |       'meta': instance.meta, | ||||||
|       'members_metioned': instance.membersMetioned, |       'members_mentioned': instance.membersMentioned, | ||||||
|       'edited_at': instance.editedAt?.toIso8601String(), |       'edited_at': instance.editedAt?.toIso8601String(), | ||||||
|       'attachments': instance.attachments.map((e) => e.toJson()).toList(), |       'attachments': instance.attachments.map((e) => e.toJson()).toList(), | ||||||
|       'reactions': instance.reactions.map((e) => e.toJson()).toList(), |       'reactions': instance.reactions.map((e) => e.toJson()).toList(), | ||||||
| @@ -227,30 +227,11 @@ Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) => | |||||||
|       'last_message': instance.lastMessage?.toJson(), |       'last_message': instance.lastMessage?.toJson(), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| _MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) => |  | ||||||
|     _MessageChange( |  | ||||||
|       messageId: json['message_id'] as String, |  | ||||||
|       action: json['action'] as String, |  | ||||||
|       message: |  | ||||||
|           json['message'] == null |  | ||||||
|               ? null |  | ||||||
|               : SnChatMessage.fromJson(json['message'] as Map<String, dynamic>), |  | ||||||
|       timestamp: DateTime.parse(json['timestamp'] as String), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
| Map<String, dynamic> _$MessageChangeToJson(_MessageChange instance) => |  | ||||||
|     <String, dynamic>{ |  | ||||||
|       'message_id': instance.messageId, |  | ||||||
|       'action': instance.action, |  | ||||||
|       'message': instance.message?.toJson(), |  | ||||||
|       'timestamp': instance.timestamp.toIso8601String(), |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
| _MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) => | _MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) => | ||||||
|     _MessageSyncResponse( |     _MessageSyncResponse( | ||||||
|       changes: |       messages: | ||||||
|           (json['changes'] as List<dynamic>?) |           (json['messages'] as List<dynamic>?) | ||||||
|               ?.map((e) => MessageChange.fromJson(e as Map<String, dynamic>)) |               ?.map((e) => SnChatMessage.fromJson(e as Map<String, dynamic>)) | ||||||
|               .toList() ?? |               .toList() ?? | ||||||
|           const [], |           const [], | ||||||
|       currentTimestamp: DateTime.parse(json['current_timestamp'] as String), |       currentTimestamp: DateTime.parse(json['current_timestamp'] as String), | ||||||
| @@ -259,7 +240,7 @@ _MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) => | |||||||
| Map<String, dynamic> _$MessageSyncResponseToJson( | Map<String, dynamic> _$MessageSyncResponseToJson( | ||||||
|   _MessageSyncResponse instance, |   _MessageSyncResponse instance, | ||||||
| ) => <String, dynamic>{ | ) => <String, dynamic>{ | ||||||
|   'changes': instance.changes.map((e) => e.toJson()).toList(), |   'messages': instance.messages.map((e) => e.toJson()).toList(), | ||||||
|   'current_timestamp': instance.currentTimestamp.toIso8601String(), |   'current_timestamp': instance.currentTimestamp.toIso8601String(), | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
|  | import 'package:island/models/file_pool.dart'; | ||||||
|  |  | ||||||
| part 'file.freezed.dart'; | part 'file.freezed.dart'; | ||||||
| part 'file.g.dart'; | part 'file.g.dart'; | ||||||
| @@ -42,6 +43,7 @@ sealed class SnCloudFile with _$SnCloudFile { | |||||||
|     required String? description, |     required String? description, | ||||||
|     required Map<String, dynamic>? fileMeta, |     required Map<String, dynamic>? fileMeta, | ||||||
|     required Map<String, dynamic>? userMeta, |     required Map<String, dynamic>? userMeta, | ||||||
|  |     required SnFilePool? pool, | ||||||
|     @Default([]) List<int> sensitiveMarks, |     @Default([]) List<int> sensitiveMarks, | ||||||
|     required String? mimeType, |     required String? mimeType, | ||||||
|     required String? hash, |     required String? hash, | ||||||
|   | |||||||
| @@ -278,7 +278,7 @@ as bool, | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnCloudFile { | mixin _$SnCloudFile { | ||||||
|  |  | ||||||
|  String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; SnFilePool? get pool; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnCloudFile | /// Create a copy of SnCloudFile | ||||||
| /// 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) | ||||||
| @@ -291,16 +291,16 @@ $SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCl | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),pool,const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -311,11 +311,11 @@ abstract mixin class $SnCloudFileCopyWith<$Res>  { | |||||||
|   factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl; |   factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | $SnFilePoolCopyWith<$Res>? get pool; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -328,14 +328,15 @@ class _$SnCloudFileCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnCloudFile | /// Create a copy of SnCloudFile | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable | ||||||
|  | as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | ||||||
| as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -347,7 +348,19 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign | |||||||
| as DateTime?, | as DateTime?, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  | /// Create a copy of SnCloudFile | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnFilePoolCopyWith<$Res>? get pool { | ||||||
|  |     if (_self.pool == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) { | ||||||
|  |     return _then(_self.copyWith(pool: value)); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -426,10 +439,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  SnFilePool? pool,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnCloudFile() when $default != null: | case _SnCloudFile() when $default != null: | ||||||
| return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -447,10 +460,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  SnFilePool? pool,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnCloudFile(): | case _SnCloudFile(): | ||||||
| return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -464,10 +477,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  SnFilePool? pool,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnCloudFile() when $default != null: | case _SnCloudFile() when $default != null: | ||||||
| return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -479,7 +492,7 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnCloudFile implements SnCloudFile { | class _SnCloudFile implements SnCloudFile { | ||||||
|   const _SnCloudFile({required this.id, required this.name, required this.description, required final  Map<String, dynamic>? fileMeta, required final  Map<String, dynamic>? userMeta, final  List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks; |   const _SnCloudFile({required this.id, required this.name, required this.description, required final  Map<String, dynamic>? fileMeta, required final  Map<String, dynamic>? userMeta, required this.pool, final  List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks; | ||||||
|   factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json); |   factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -503,6 +516,7 @@ class _SnCloudFile implements SnCloudFile { | |||||||
|   return EqualUnmodifiableMapView(value); |   return EqualUnmodifiableMapView(value); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @override final  SnFilePool? pool; | ||||||
|  final  List<int> _sensitiveMarks; |  final  List<int> _sensitiveMarks; | ||||||
| @override@JsonKey() List<int> get sensitiveMarks { | @override@JsonKey() List<int> get sensitiveMarks { | ||||||
|   if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks; |   if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks; | ||||||
| @@ -532,16 +546,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),pool,const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -552,11 +566,11 @@ abstract mixin class _$SnCloudFileCopyWith<$Res> implements $SnCloudFileCopyWith | |||||||
|   factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl; |   factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override $SnFilePoolCopyWith<$Res>? get pool; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -569,14 +583,15 @@ class __$SnCloudFileCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnCloudFile | /// Create a copy of SnCloudFile | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnCloudFile( |   return _then(_SnCloudFile( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,fileMeta: freezed == fileMeta ? _self._fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | as String?,fileMeta: freezed == fileMeta ? _self._fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userMeta // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable | ||||||
|  | as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | ||||||
| as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -589,7 +604,19 @@ as DateTime?, | |||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Create a copy of SnCloudFile | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnFilePoolCopyWith<$Res>? get pool { | ||||||
|  |     if (_self.pool == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) { | ||||||
|  |     return _then(_self.copyWith(pool: value)); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| } | } | ||||||
|  |  | ||||||
| // dart format on | // dart format on | ||||||
|   | |||||||
| @@ -33,6 +33,10 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile( | |||||||
|   description: json['description'] as String?, |   description: json['description'] as String?, | ||||||
|   fileMeta: json['file_meta'] as Map<String, dynamic>?, |   fileMeta: json['file_meta'] as Map<String, dynamic>?, | ||||||
|   userMeta: json['user_meta'] as Map<String, dynamic>?, |   userMeta: json['user_meta'] as Map<String, dynamic>?, | ||||||
|  |   pool: | ||||||
|  |       json['pool'] == null | ||||||
|  |           ? null | ||||||
|  |           : SnFilePool.fromJson(json['pool'] as Map<String, dynamic>), | ||||||
|   sensitiveMarks: |   sensitiveMarks: | ||||||
|       (json['sensitive_marks'] as List<dynamic>?) |       (json['sensitive_marks'] as List<dynamic>?) | ||||||
|           ?.map((e) => (e as num).toInt()) |           ?.map((e) => (e as num).toInt()) | ||||||
| @@ -61,6 +65,7 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) => | |||||||
|       'description': instance.description, |       'description': instance.description, | ||||||
|       'file_meta': instance.fileMeta, |       'file_meta': instance.fileMeta, | ||||||
|       'user_meta': instance.userMeta, |       'user_meta': instance.userMeta, | ||||||
|  |       'pool': instance.pool?.toJson(), | ||||||
|       'sensitive_marks': instance.sensitiveMarks, |       'sensitive_marks': instance.sensitiveMarks, | ||||||
|       'mime_type': instance.mimeType, |       'mime_type': instance.mimeType, | ||||||
|       'hash': instance.hash, |       'hash': instance.hash, | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								lib/models/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/models/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
|  |  | ||||||
|  | part 'file_pool.freezed.dart'; | ||||||
|  | part 'file_pool.g.dart'; | ||||||
|  |  | ||||||
|  | @freezed | ||||||
|  | sealed class SnFilePool with _$SnFilePool { | ||||||
|  |   const factory SnFilePool({ | ||||||
|  |     required String id, | ||||||
|  |     required String name, | ||||||
|  |     String? description, | ||||||
|  |     Map<String, dynamic>? storageConfig, | ||||||
|  |     Map<String, dynamic>? billingConfig, | ||||||
|  |     Map<String, dynamic>? policyConfig, | ||||||
|  |     bool? isHidden, | ||||||
|  |     String? accountId, | ||||||
|  |     String? resourceIdentifier, | ||||||
|  |     DateTime? createdAt, | ||||||
|  |     DateTime? updatedAt, | ||||||
|  |     DateTime? deletedAt, | ||||||
|  |   }) = _SnFilePool; | ||||||
|  |  | ||||||
|  |   factory SnFilePool.fromJson(Map<String, dynamic> json) => | ||||||
|  |       _$SnFilePoolFromJson(json); | ||||||
|  | } | ||||||
							
								
								
									
										328
									
								
								lib/models/file_pool.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								lib/models/file_pool.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,328 @@ | |||||||
|  | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
|  | // coverage:ignore-file | ||||||
|  | // ignore_for_file: type=lint | ||||||
|  | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark | ||||||
|  |  | ||||||
|  | part of 'file_pool.dart'; | ||||||
|  |  | ||||||
|  | // ************************************************************************** | ||||||
|  | // FreezedGenerator | ||||||
|  | // ************************************************************************** | ||||||
|  |  | ||||||
|  | // dart format off | ||||||
|  | T _$identity<T>(T value) => value; | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | mixin _$SnFilePool { | ||||||
|  |  | ||||||
|  |  String get id; String get name; String? get description; Map<String, dynamic>? get storageConfig; Map<String, dynamic>? get billingConfig; Map<String, dynamic>? get policyConfig; bool? get isHidden; String? get accountId; String? get resourceIdentifier; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; | ||||||
|  | /// Create a copy of SnFilePool | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnFilePoolCopyWith<SnFilePool> get copyWith => _$SnFilePoolCopyWithImpl<SnFilePool>(this as SnFilePool, _$identity); | ||||||
|  |  | ||||||
|  |   /// Serializes this SnFilePool to a JSON map. | ||||||
|  |   Map<String, dynamic> toJson(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.storageConfig, storageConfig)&&const DeepCollectionEquality().equals(other.billingConfig, billingConfig)&&const DeepCollectionEquality().equals(other.policyConfig, policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(storageConfig),const DeepCollectionEquality().hash(billingConfig),const DeepCollectionEquality().hash(policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class $SnFilePoolCopyWith<$Res>  { | ||||||
|  |   factory $SnFilePoolCopyWith(SnFilePool value, $Res Function(SnFilePool) _then) = _$SnFilePoolCopyWithImpl; | ||||||
|  | @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class _$SnFilePoolCopyWithImpl<$Res> | ||||||
|  |     implements $SnFilePoolCopyWith<$Res> { | ||||||
|  |   _$SnFilePoolCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final SnFilePool _self; | ||||||
|  |   final $Res Function(SnFilePool) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnFilePool | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) { | ||||||
|  |   return _then(_self.copyWith( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,storageConfig: freezed == storageConfig ? _self.storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self.billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self.policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// Adds pattern-matching-related methods to [SnFilePool]. | ||||||
|  | extension SnFilePoolPatterns on SnFilePool { | ||||||
|  | /// 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( _SnFilePool value)?  $default,{required TResult orElse(),}){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnFilePool() 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( _SnFilePool value)  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnFilePool(): | ||||||
|  | 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( _SnFilePool value)?  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnFilePool() when $default != null: | ||||||
|  | return $default(_that);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to an `orElse` callback. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return orElse(); | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? storageConfig,  Map<String, dynamic>? billingConfig,  Map<String, dynamic>? policyConfig,  bool? isHidden,  String? accountId,  String? resourceIdentifier,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnFilePool() when $default != null: | ||||||
|  | return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|  |   return orElse(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A `switch`-like method, using callbacks. | ||||||
|  | /// | ||||||
|  | /// As opposed to `map`, this offers destructuring. | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case Subclass2(:final field2): | ||||||
|  | ///     return ...; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? storageConfig,  Map<String, dynamic>? billingConfig,  Map<String, dynamic>? policyConfig,  bool? isHidden,  String? accountId,  String? resourceIdentifier,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnFilePool(): | ||||||
|  | return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to returning `null` | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return null; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String? description,  Map<String, dynamic>? storageConfig,  Map<String, dynamic>? billingConfig,  Map<String, dynamic>? policyConfig,  bool? isHidden,  String? accountId,  String? resourceIdentifier,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnFilePool() when $default != null: | ||||||
|  | return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | @JsonSerializable() | ||||||
|  |  | ||||||
|  | class _SnFilePool implements SnFilePool { | ||||||
|  |   const _SnFilePool({required this.id, required this.name, this.description, final  Map<String, dynamic>? storageConfig, final  Map<String, dynamic>? billingConfig, final  Map<String, dynamic>? policyConfig, this.isHidden, this.accountId, this.resourceIdentifier, this.createdAt, this.updatedAt, this.deletedAt}): _storageConfig = storageConfig,_billingConfig = billingConfig,_policyConfig = policyConfig; | ||||||
|  |   factory _SnFilePool.fromJson(Map<String, dynamic> json) => _$SnFilePoolFromJson(json); | ||||||
|  |  | ||||||
|  | @override final  String id; | ||||||
|  | @override final  String name; | ||||||
|  | @override final  String? description; | ||||||
|  |  final  Map<String, dynamic>? _storageConfig; | ||||||
|  | @override Map<String, dynamic>? get storageConfig { | ||||||
|  |   final value = _storageConfig; | ||||||
|  |   if (value == null) return null; | ||||||
|  |   if (_storageConfig is EqualUnmodifiableMapView) return _storageConfig; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableMapView(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  final  Map<String, dynamic>? _billingConfig; | ||||||
|  | @override Map<String, dynamic>? get billingConfig { | ||||||
|  |   final value = _billingConfig; | ||||||
|  |   if (value == null) return null; | ||||||
|  |   if (_billingConfig is EqualUnmodifiableMapView) return _billingConfig; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableMapView(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  final  Map<String, dynamic>? _policyConfig; | ||||||
|  | @override Map<String, dynamic>? get policyConfig { | ||||||
|  |   final value = _policyConfig; | ||||||
|  |   if (value == null) return null; | ||||||
|  |   if (_policyConfig is EqualUnmodifiableMapView) return _policyConfig; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableMapView(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @override final  bool? isHidden; | ||||||
|  | @override final  String? accountId; | ||||||
|  | @override final  String? resourceIdentifier; | ||||||
|  | @override final  DateTime? createdAt; | ||||||
|  | @override final  DateTime? updatedAt; | ||||||
|  | @override final  DateTime? deletedAt; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnFilePool | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | _$SnFilePoolCopyWith<_SnFilePool> get copyWith => __$SnFilePoolCopyWithImpl<_SnFilePool>(this, _$identity); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | Map<String, dynamic> toJson() { | ||||||
|  |   return _$SnFilePoolToJson(this, ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._storageConfig, _storageConfig)&&const DeepCollectionEquality().equals(other._billingConfig, _billingConfig)&&const DeepCollectionEquality().equals(other._policyConfig, _policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_storageConfig),const DeepCollectionEquality().hash(_billingConfig),const DeepCollectionEquality().hash(_policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class _$SnFilePoolCopyWith<$Res> implements $SnFilePoolCopyWith<$Res> { | ||||||
|  |   factory _$SnFilePoolCopyWith(_SnFilePool value, $Res Function(_SnFilePool) _then) = __$SnFilePoolCopyWithImpl; | ||||||
|  | @override @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class __$SnFilePoolCopyWithImpl<$Res> | ||||||
|  |     implements _$SnFilePoolCopyWith<$Res> { | ||||||
|  |   __$SnFilePoolCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final _SnFilePool _self; | ||||||
|  |   final $Res Function(_SnFilePool) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnFilePool | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) { | ||||||
|  |   return _then(_SnFilePool( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,storageConfig: freezed == storageConfig ? _self._storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self._billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self._policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // dart format on | ||||||
							
								
								
									
										47
									
								
								lib/models/file_pool.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/models/file_pool.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
|  |  | ||||||
|  | part of 'file_pool.dart'; | ||||||
|  |  | ||||||
|  | // ************************************************************************** | ||||||
|  | // JsonSerializableGenerator | ||||||
|  | // ************************************************************************** | ||||||
|  |  | ||||||
|  | _SnFilePool _$SnFilePoolFromJson(Map<String, dynamic> json) => _SnFilePool( | ||||||
|  |   id: json['id'] as String, | ||||||
|  |   name: json['name'] as String, | ||||||
|  |   description: json['description'] as String?, | ||||||
|  |   storageConfig: json['storage_config'] as Map<String, dynamic>?, | ||||||
|  |   billingConfig: json['billing_config'] as Map<String, dynamic>?, | ||||||
|  |   policyConfig: json['policy_config'] as Map<String, dynamic>?, | ||||||
|  |   isHidden: json['is_hidden'] as bool?, | ||||||
|  |   accountId: json['account_id'] as String?, | ||||||
|  |   resourceIdentifier: json['resource_identifier'] as String?, | ||||||
|  |   createdAt: | ||||||
|  |       json['created_at'] == null | ||||||
|  |           ? null | ||||||
|  |           : DateTime.parse(json['created_at'] as String), | ||||||
|  |   updatedAt: | ||||||
|  |       json['updated_at'] == null | ||||||
|  |           ? null | ||||||
|  |           : DateTime.parse(json['updated_at'] as String), | ||||||
|  |   deletedAt: | ||||||
|  |       json['deleted_at'] == null | ||||||
|  |           ? null | ||||||
|  |           : DateTime.parse(json['deleted_at'] as String), | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | Map<String, dynamic> _$SnFilePoolToJson(_SnFilePool instance) => | ||||||
|  |     <String, dynamic>{ | ||||||
|  |       'id': instance.id, | ||||||
|  |       'name': instance.name, | ||||||
|  |       'description': instance.description, | ||||||
|  |       'storage_config': instance.storageConfig, | ||||||
|  |       'billing_config': instance.billingConfig, | ||||||
|  |       'policy_config': instance.policyConfig, | ||||||
|  |       'is_hidden': instance.isHidden, | ||||||
|  |       'account_id': instance.accountId, | ||||||
|  |       'resource_identifier': instance.resourceIdentifier, | ||||||
|  |       'created_at': instance.createdAt?.toIso8601String(), | ||||||
|  |       'updated_at': instance.updatedAt?.toIso8601String(), | ||||||
|  |       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||||
|  |     }; | ||||||
| @@ -4,12 +4,16 @@ import 'dart:developer' as developer; | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/widgets/account/status.dart'; | ||||||
| import 'package:shelf/shelf.dart'; | import 'package:shelf/shelf.dart'; | ||||||
| import 'package:shelf/shelf_io.dart' as shelf_io; | import 'package:shelf/shelf_io.dart' as shelf_io; | ||||||
| import 'package:shelf_web_socket/shelf_web_socket.dart'; | import 'package:shelf_web_socket/shelf_web_socket.dart'; | ||||||
| import 'package:web_socket_channel/web_socket_channel.dart'; | import 'package:web_socket_channel/web_socket_channel.dart'; | ||||||
| import 'package:path/path.dart' as path; | 
 | ||||||
|  | // Conditional imports for IPC server - use web stubs on web platform | ||||||
|  | import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart'; | ||||||
| 
 | 
 | ||||||
| const String kRpcLogPrefix = 'arRPC.websocket'; | const String kRpcLogPrefix = 'arRPC.websocket'; | ||||||
| const String kRpcIpcLogPrefix = 'arRPC.ipc'; | const String kRpcIpcLogPrefix = 'arRPC.ipc'; | ||||||
| @@ -43,14 +47,14 @@ class IpcErrorCodes { | |||||||
|   static const int invalidEncoding = 4005; |   static const int invalidEncoding = 4005; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js | ||||||
| class ActivityRpcServer { | class ActivityRpcServer { | ||||||
|   static const List<int> portRange = [6463, 6472]; // Ports 6463–6472 |   static const List<int> portRange = [6463, 6472]; // Ports 6463–6472 | ||||||
|   Map<String, Function> |   Map<String, Function> | ||||||
|   handlers; // {connection: (socket), message: (socket, data), close: (socket)} |   handlers; // {connection: (socket), message: (socket, data), close: (socket)} | ||||||
|   HttpServer? _httpServer; |   HttpServer? _httpServer; | ||||||
|   ServerSocket? _ipcServer; |   IpcServer? _ipcServer; | ||||||
|   final List<WebSocketChannel> _wsSockets = []; |   final List<WebSocketChannel> _wsSockets = []; | ||||||
|   final List<_IpcSocketWrapper> _ipcSockets = []; |  | ||||||
| 
 | 
 | ||||||
|   ActivityRpcServer(this.handlers); |   ActivityRpcServer(this.handlers); | ||||||
| 
 | 
 | ||||||
| @@ -58,109 +62,20 @@ class ActivityRpcServer { | |||||||
|     handlers = newHandlers; |     handlers = newHandlers; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Encode IPC packet |   // Start the server | ||||||
|   static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) { |  | ||||||
|     final jsonData = jsonEncode(data); |  | ||||||
|     final dataBytes = utf8.encode(jsonData); |  | ||||||
|     final dataSize = dataBytes.length; |  | ||||||
| 
 |  | ||||||
|     final buffer = ByteData(8 + dataSize); |  | ||||||
|     buffer.setInt32(0, type, Endian.little); |  | ||||||
|     buffer.setInt32(4, dataSize, Endian.little); |  | ||||||
|     buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes); |  | ||||||
| 
 |  | ||||||
|     return buffer.buffer.asUint8List(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   Future<String> _getMacOsSystemTmpDir() async { |  | ||||||
|     final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']); |  | ||||||
|     return (result.stdout as String).trim(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Find available IPC socket path |  | ||||||
|   Future<String> _findAvailableIpcPath() async { |  | ||||||
|     // Build list of directories to try, with macOS-specific handling |  | ||||||
|     final baseDirs = <String>[]; |  | ||||||
| 
 |  | ||||||
|     if (Platform.isMacOS) { |  | ||||||
|       try { |  | ||||||
|         final macTempDir = await _getMacOsSystemTmpDir(); |  | ||||||
|         if (macTempDir.isNotEmpty) { |  | ||||||
|           baseDirs.add(macTempDir); |  | ||||||
|         } |  | ||||||
|       } catch (e) { |  | ||||||
|         developer.log( |  | ||||||
|           'Failed to get macOS system temp dir: $e', |  | ||||||
|           name: kRpcIpcLogPrefix, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Add other standard directories |  | ||||||
|     final otherDirs = [ |  | ||||||
|       Platform.environment['XDG_RUNTIME_DIR'], // User runtime directory |  | ||||||
|       Platform.environment['TMPDIR'], // App container temp (fallback) |  | ||||||
|       Platform.environment['TMP'], |  | ||||||
|       Platform.environment['TEMP'], |  | ||||||
|       '/tmp', // System temp directory - most compatible |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     baseDirs.addAll( |  | ||||||
|       otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     for (final baseDir in baseDirs) { |  | ||||||
|       for (int i = 0; i < 10; i++) { |  | ||||||
|         final socketPath = path.join(baseDir, '$kIpcBasePath-$i'); |  | ||||||
|         try { |  | ||||||
|           final socket = await ServerSocket.bind( |  | ||||||
|             InternetAddress(socketPath, type: InternetAddressType.unix), |  | ||||||
|             0, |  | ||||||
|           ); |  | ||||||
|           socket.close(); |  | ||||||
|           // Clean up the test socket |  | ||||||
|           try { |  | ||||||
|             await File(socketPath).delete(); |  | ||||||
|           } catch (_) {} |  | ||||||
|           developer.log( |  | ||||||
|             'IPC socket will be created at: $socketPath', |  | ||||||
|             name: kRpcIpcLogPrefix, |  | ||||||
|           ); |  | ||||||
|           return socketPath; |  | ||||||
|         } catch (e) { |  | ||||||
|           // Path not available, try next |  | ||||||
|           if (i == 0) { |  | ||||||
|             // Log only for the first attempt per directory |  | ||||||
|             developer.log( |  | ||||||
|               'IPC path $socketPath not available: $e', |  | ||||||
|               name: kRpcIpcLogPrefix, |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     throw Exception( |  | ||||||
|       'No available IPC socket paths found in any temp directory', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Start the WebSocket server |  | ||||||
|   Future<void> start() async { |   Future<void> start() async { | ||||||
|     int port = portRange[0]; |     int port = portRange[0]; | ||||||
|     bool wsSuccess = false; |     bool wsSuccess = false; | ||||||
| 
 | 
 | ||||||
|     // Start WebSocket server |     // Start WebSocket server | ||||||
|     while (port <= portRange[1]) { |     while (port <= portRange[1]) { | ||||||
|       developer.log('trying port $port', name: kRpcLogPrefix); |       developer.log('Trying port $port', name: kRpcLogPrefix); | ||||||
|       try { |       try { | ||||||
|         // Start HTTP server |  | ||||||
|         _httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port); |         _httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port); | ||||||
|         developer.log('listening on $port', name: kRpcLogPrefix); |         developer.log('Listening on $port', name: kRpcLogPrefix); | ||||||
| 
 | 
 | ||||||
|         // Handle WebSocket upgrades |  | ||||||
|         shelf_io.serveRequests(_httpServer!, (Request request) async { |         shelf_io.serveRequests(_httpServer!, (Request request) async { | ||||||
|           developer.log('new request', name: kRpcLogPrefix); |           developer.log('New request', name: kRpcLogPrefix); | ||||||
|           if (request.headers['upgrade']?.toLowerCase() == 'websocket') { |           if (request.headers['upgrade']?.toLowerCase() == 'websocket') { | ||||||
|             final handler = webSocketHandler((WebSocketChannel channel, _) { |             final handler = webSocketHandler((WebSocketChannel channel, _) { | ||||||
|               _wsSockets.add(channel); |               _wsSockets.add(channel); | ||||||
| @@ -169,7 +84,7 @@ class ActivityRpcServer { | |||||||
|             return handler(request); |             return handler(request); | ||||||
|           } |           } | ||||||
|           developer.log( |           developer.log( | ||||||
|             'new request disposed due to not websocket', |             'New request disposed due to not websocket', | ||||||
|             name: kRpcLogPrefix, |             name: kRpcLogPrefix, | ||||||
|           ); |           ); | ||||||
|           return Response.notFound('Not a WebSocket request'); |           return Response.notFound('Not a WebSocket request'); | ||||||
| @@ -178,12 +93,12 @@ class ActivityRpcServer { | |||||||
|         break; |         break; | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         if (e is SocketException && e.osError?.errorCode == 98) { |         if (e is SocketException && e.osError?.errorCode == 98) { | ||||||
|           // EADDRINUSE |  | ||||||
|           developer.log('$port in use!', name: kRpcLogPrefix); |           developer.log('$port in use!', name: kRpcLogPrefix); | ||||||
|         } else { |         } else { | ||||||
|           developer.log('http error: $e', name: kRpcLogPrefix); |           developer.log('HTTP error: $e', name: kRpcLogPrefix); | ||||||
|         } |         } | ||||||
|         port++; |         port++; | ||||||
|  |         await Future.delayed(Duration(milliseconds: 100)); // Add delay | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -193,27 +108,24 @@ class ActivityRpcServer { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Start IPC server (skip on macOS due to sandboxing) |     // Start IPC server | ||||||
|     final shouldStartIpc = !Platform.isMacOS; |     final shouldStartIpc = !Platform.isMacOS && !kIsWeb; | ||||||
|     if (shouldStartIpc) { |     if (shouldStartIpc) { | ||||||
|       try { |       try { | ||||||
|         final ipcPath = await _findAvailableIpcPath(); |         _ipcServer = MultiPlatformIpcServer(); | ||||||
|         _ipcServer = await ServerSocket.bind( |  | ||||||
|           InternetAddress(ipcPath, type: InternetAddressType.unix), |  | ||||||
|           0, |  | ||||||
|         ); |  | ||||||
|         developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix); |  | ||||||
| 
 | 
 | ||||||
|         _ipcServer!.listen((Socket socket) { |         // Set up IPC handlers | ||||||
|           _onIpcConnection(socket); |         _ipcServer!.handlePacket = (socket, packet, _) { | ||||||
|         }); |           _handleIpcPacket(socket, packet); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await _ipcServer!.start(); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         developer.log('IPC server error: $e', name: kRpcIpcLogPrefix); |         developer.log('IPC server error: $e', name: kRpcIpcLogPrefix); | ||||||
|         // Continue without IPC if it fails |  | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       developer.log( |       developer.log( | ||||||
|         'IPC server disabled on macOS in production mode due to sandboxing', |         'IPC server disabled on macOS or web in production mode', | ||||||
|         name: kRpcIpcLogPrefix, |         name: kRpcIpcLogPrefix, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -223,24 +135,23 @@ class ActivityRpcServer { | |||||||
|   Future<void> stop() async { |   Future<void> stop() async { | ||||||
|     // Stop WebSocket server |     // Stop WebSocket server | ||||||
|     for (var socket in _wsSockets) { |     for (var socket in _wsSockets) { | ||||||
|  |       try { | ||||||
|         await socket.sink.close(); |         await socket.sink.close(); | ||||||
|  |       } catch (e) { | ||||||
|  |         developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     _wsSockets.clear(); |     _wsSockets.clear(); | ||||||
|     await _httpServer?.close(); |     await _httpServer?.close(force: true); | ||||||
| 
 | 
 | ||||||
|     // Stop IPC server |     // Stop IPC server | ||||||
|     for (var socket in _ipcSockets) { |     await _ipcServer?.stop(); | ||||||
|       socket.close(); |  | ||||||
|     } |  | ||||||
|     _ipcSockets.clear(); |  | ||||||
|     await _ipcServer?.close(); |  | ||||||
| 
 | 
 | ||||||
|     developer.log('servers stopped', name: kRpcLogPrefix); |     developer.log('Servers stopped', name: kRpcLogPrefix); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Handle new WebSocket connection |   // Handle new WebSocket connection | ||||||
|   void _onWsConnection(WebSocketChannel socket, Request request) { |   void _onWsConnection(WebSocketChannel socket, Request request) { | ||||||
|     // Parse query parameters |  | ||||||
|     final uri = request.url; |     final uri = request.url; | ||||||
|     final params = uri.queryParameters; |     final params = uri.queryParameters; | ||||||
|     final ver = int.tryParse(params['v'] ?? '1') ?? 1; |     final ver = int.tryParse(params['v'] ?? '1') ?? 1; | ||||||
| @@ -249,43 +160,38 @@ class ActivityRpcServer { | |||||||
|     final origin = request.headers['origin'] ?? ''; |     final origin = request.headers['origin'] ?? ''; | ||||||
| 
 | 
 | ||||||
|     developer.log( |     developer.log( | ||||||
|       'new WS connection! origin: $origin, params: $params', |       'New WS connection! origin: $origin, params: $params', | ||||||
|       name: kRpcLogPrefix, |       name: kRpcLogPrefix, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // Validate origin |  | ||||||
|     if (origin.isNotEmpty && |     if (origin.isNotEmpty && | ||||||
|         ![ |         ![ | ||||||
|           'https://discord.com', |           'https://discord.com', | ||||||
|           'https://ptb.discord.com', |           'https://ptb.discord.com', | ||||||
|           'https://canary.discord.com', |           'https://canary.discord.com', | ||||||
|         ].contains(origin)) { |         ].contains(origin)) { | ||||||
|       developer.log('disallowed origin: $origin', name: kRpcLogPrefix); |       developer.log('Disallowed origin: $origin', name: kRpcLogPrefix); | ||||||
|       socket.sink.close(); |       socket.sink.close(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Validate encoding |  | ||||||
|     if (encoding != 'json') { |     if (encoding != 'json') { | ||||||
|       developer.log( |       developer.log( | ||||||
|         'unsupported encoding requested: $encoding', |         'Unsupported encoding requested: $encoding', | ||||||
|         name: kRpcLogPrefix, |         name: kRpcLogPrefix, | ||||||
|       ); |       ); | ||||||
|       socket.sink.close(); |       socket.sink.close(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Validate version |  | ||||||
|     if (ver != 1) { |     if (ver != 1) { | ||||||
|       developer.log('unsupported version requested: $ver', name: kRpcLogPrefix); |       developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix); | ||||||
|       socket.sink.close(); |       socket.sink.close(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Store client info on socket |  | ||||||
|     final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding); |     final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding); | ||||||
| 
 | 
 | ||||||
|     // Set up event listeners |  | ||||||
|     socket.stream.listen( |     socket.stream.listen( | ||||||
|       (data) => _onWsMessage(socketWithMeta, data), |       (data) => _onWsMessage(socketWithMeta, data), | ||||||
|       onError: (e) { |       onError: (e) { | ||||||
| @@ -298,36 +204,27 @@ class ActivityRpcServer { | |||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // Notify handler of new connection |  | ||||||
|     handlers['connection']?.call(socketWithMeta); |     handlers['connection']?.call(socketWithMeta); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Handle new IPC connection |  | ||||||
|   void _onIpcConnection(Socket socket) { |  | ||||||
|     developer.log('new IPC connection!', name: kRpcIpcLogPrefix); |  | ||||||
| 
 |  | ||||||
|     final socketWrapper = _IpcSocketWrapper(socket); |  | ||||||
|     _ipcSockets.add(socketWrapper); |  | ||||||
| 
 |  | ||||||
|     // Set up event listeners |  | ||||||
|     socket.listen( |  | ||||||
|       (data) => _onIpcData(socketWrapper, data), |  | ||||||
|       onError: (e) { |  | ||||||
|         developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix); |  | ||||||
|         socket.close(); |  | ||||||
|       }, |  | ||||||
|       onDone: () { |  | ||||||
|         developer.log('IPC socket closed', name: kRpcIpcLogPrefix); |  | ||||||
|         handlers['close']?.call(socketWrapper); |  | ||||||
|         _ipcSockets.remove(socketWrapper); |  | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Handle incoming WebSocket message |   // Handle incoming WebSocket message | ||||||
|   void _onWsMessage(_WsSocketWrapper socket, dynamic data) { |   Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async { | ||||||
|  |     if (data is! String) { | ||||||
|  |       developer.log( | ||||||
|  |         'Invalid WebSocket message: not a string', | ||||||
|  |         name: kRpcLogPrefix, | ||||||
|  |       ); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       final jsonData = jsonDecode(data as String); |       final jsonData = await compute(jsonDecode, data); | ||||||
|  |       if (jsonData is! Map<String, dynamic>) { | ||||||
|  |         developer.log( | ||||||
|  |           'Invalid WebSocket message: not a JSON object', | ||||||
|  |           name: kRpcLogPrefix, | ||||||
|  |         ); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|       developer.log('WS message: $jsonData', name: kRpcLogPrefix); |       developer.log('WS message: $jsonData', name: kRpcLogPrefix); | ||||||
|       handlers['message']?.call(socket, jsonData); |       handlers['message']?.call(socket, jsonData); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @@ -335,22 +232,8 @@ class ActivityRpcServer { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Handle incoming IPC data |  | ||||||
|   void _onIpcData(_IpcSocketWrapper socket, List<int> data) { |  | ||||||
|     try { |  | ||||||
|       socket.addData(data); |  | ||||||
|       final packets = socket.readPackets(); |  | ||||||
|       for (final packet in packets) { |  | ||||||
|         _handleIpcPacket(socket, packet); |  | ||||||
|       } |  | ||||||
|     } catch (e) { |  | ||||||
|       developer.log('IPC data error: $e', name: kRpcIpcLogPrefix); |  | ||||||
|       socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString()); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Handle IPC packet |   // Handle IPC packet | ||||||
|   void _handleIpcPacket(_IpcSocketWrapper socket, _IpcPacket packet) { |   void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) { | ||||||
|     switch (packet.type) { |     switch (packet.type) { | ||||||
|       case IpcTypes.ping: |       case IpcTypes.ping: | ||||||
|         developer.log('IPC ping received', name: kRpcIpcLogPrefix); |         developer.log('IPC ping received', name: kRpcIpcLogPrefix); | ||||||
| @@ -359,7 +242,6 @@ class ActivityRpcServer { | |||||||
| 
 | 
 | ||||||
|       case IpcTypes.pong: |       case IpcTypes.pong: | ||||||
|         developer.log('IPC pong received', name: kRpcIpcLogPrefix); |         developer.log('IPC pong received', name: kRpcIpcLogPrefix); | ||||||
|         // Handle pong if needed |  | ||||||
|         break; |         break; | ||||||
| 
 | 
 | ||||||
|       case IpcTypes.handshake: |       case IpcTypes.handshake: | ||||||
| @@ -388,13 +270,12 @@ class ActivityRpcServer { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Handle IPC handshake |   // Handle IPC handshake | ||||||
|   void _onIpcHandshake(_IpcSocketWrapper socket, Map<String, dynamic> params) { |   void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) { | ||||||
|     developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix); |     developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix); | ||||||
| 
 | 
 | ||||||
|     final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1; |     final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1; | ||||||
|     final clientId = params['client_id']?.toString() ?? ''; |     final clientId = params['client_id']?.toString() ?? ''; | ||||||
| 
 | 
 | ||||||
|     // Validate version |  | ||||||
|     if (ver != 1) { |     if (ver != 1) { | ||||||
|       developer.log( |       developer.log( | ||||||
|         'IPC unsupported version requested: $ver', |         'IPC unsupported version requested: $ver', | ||||||
| @@ -404,7 +285,6 @@ class ActivityRpcServer { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Validate client ID |  | ||||||
|     if (clientId.isEmpty) { |     if (clientId.isEmpty) { | ||||||
|       developer.log('IPC client ID required', name: kRpcIpcLogPrefix); |       developer.log('IPC client ID required', name: kRpcIpcLogPrefix); | ||||||
|       socket.closeWithCode(IpcErrorCodes.invalidClientId); |       socket.closeWithCode(IpcErrorCodes.invalidClientId); | ||||||
| @@ -413,7 +293,6 @@ class ActivityRpcServer { | |||||||
| 
 | 
 | ||||||
|     socket.clientId = clientId; |     socket.clientId = clientId; | ||||||
| 
 | 
 | ||||||
|     // Notify handler of new connection |  | ||||||
|     handlers['connection']?.call(socket); |     handlers['connection']?.call(socket); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -432,74 +311,6 @@ class _WsSocketWrapper { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // IPC wrapper |  | ||||||
| class _IpcSocketWrapper { |  | ||||||
|   final Socket socket; |  | ||||||
|   String clientId = ''; |  | ||||||
|   bool handshook = false; |  | ||||||
|   final List<int> _buffer = []; |  | ||||||
| 
 |  | ||||||
|   _IpcSocketWrapper(this.socket); |  | ||||||
| 
 |  | ||||||
|   void addData(List<int> data) { |  | ||||||
|     _buffer.addAll(data); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   void send(Map<String, dynamic> msg) { |  | ||||||
|     developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix); |  | ||||||
|     final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.frame, msg); |  | ||||||
|     socket.add(packet); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   void sendPong(dynamic data) { |  | ||||||
|     final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {}); |  | ||||||
|     socket.add(packet); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   void close() { |  | ||||||
|     socket.close(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   void closeWithCode(int code, [String message = '']) { |  | ||||||
|     final closeData = {'code': code, 'message': message}; |  | ||||||
|     final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.close, closeData); |  | ||||||
|     socket.add(packet); |  | ||||||
|     socket.close(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   List<_IpcPacket> readPackets() { |  | ||||||
|     final packets = <_IpcPacket>[]; |  | ||||||
| 
 |  | ||||||
|     while (_buffer.length >= 8) { |  | ||||||
|       final buffer = Uint8List.fromList(_buffer); |  | ||||||
|       final byteData = ByteData.view(buffer.buffer); |  | ||||||
| 
 |  | ||||||
|       final type = byteData.getInt32(0, Endian.little); |  | ||||||
|       final dataSize = byteData.getInt32(4, Endian.little); |  | ||||||
| 
 |  | ||||||
|       if (_buffer.length < 8 + dataSize) break; |  | ||||||
| 
 |  | ||||||
|       final dataBytes = _buffer.sublist(8, 8 + dataSize); |  | ||||||
|       final jsonStr = utf8.decode(dataBytes); |  | ||||||
|       final jsonData = jsonDecode(jsonStr); |  | ||||||
| 
 |  | ||||||
|       packets.add(_IpcPacket(type, jsonData)); |  | ||||||
| 
 |  | ||||||
|       _buffer.removeRange(0, 8 + dataSize); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return packets; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // IPC Packet structure |  | ||||||
| class _IpcPacket { |  | ||||||
|   final int type; |  | ||||||
|   final Map<String, dynamic> data; |  | ||||||
| 
 |  | ||||||
|   _IpcPacket(this.type, this.data); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // State management for server status and activities | // State management for server status and activities | ||||||
| class ServerState { | class ServerState { | ||||||
|   final String status; |   final String status; | ||||||
| @@ -522,7 +333,6 @@ class ServerStateNotifier extends StateNotifier<ServerState> { | |||||||
|     : super(ServerState(status: 'Server not started')); |     : super(ServerState(status: 'Server not started')); | ||||||
| 
 | 
 | ||||||
|   Future<void> start() async { |   Future<void> start() async { | ||||||
|     // Only start server on desktop platforms |  | ||||||
|     if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) { |     if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) { | ||||||
|       try { |       try { | ||||||
|         await server.start(); |         await server.start(); | ||||||
| @@ -531,7 +341,9 @@ class ServerStateNotifier extends StateNotifier<ServerState> { | |||||||
|         state = state.copyWith(status: 'Server failed: $e'); |         state = state.copyWith(status: 'Server failed: $e'); | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|  |       Future(() { | ||||||
|         state = state.copyWith(status: 'Server disabled on mobile/web'); |         state = state.copyWith(status: 'Server disabled on mobile/web'); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -554,9 +366,8 @@ final rpcServerStateProvider = | |||||||
|           final clientId = |           final clientId = | ||||||
|               socket is _WsSocketWrapper |               socket is _WsSocketWrapper | ||||||
|                   ? socket.clientId |                   ? socket.clientId | ||||||
|                   : (socket as _IpcSocketWrapper).clientId; |                   : (socket as IpcSocketWrapper).clientId; | ||||||
|           notifier.updateStatus('Client connected (ID: $clientId)'); |           notifier.updateStatus('Client connected (ID: $clientId)'); | ||||||
|           // Send READY event |  | ||||||
|           socket.send({ |           socket.send({ | ||||||
|             'cmd': 'DISPATCH', |             'cmd': 'DISPATCH', | ||||||
|             'data': { |             'data': { | ||||||
| @@ -575,26 +386,47 @@ final rpcServerStateProvider = | |||||||
|               }, |               }, | ||||||
|             }, |             }, | ||||||
|             'evt': 'READY', |             'evt': 'READY', | ||||||
|             'nonce': '12345', // Should be dynamic |             'nonce': '12345', | ||||||
|           }); |           }); | ||||||
|         }, |         }, | ||||||
|         'message': (socket, dynamic data) async { |         'message': (socket, dynamic data) async { | ||||||
|           if (data['cmd'] == 'SET_ACTIVITY') { |           if (data['cmd'] == 'SET_ACTIVITY') { | ||||||
|             notifier.addActivity( |             notifier.addActivity( | ||||||
|               'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}', |               'Activity: ${data['args']['activity']['details'] ?? ''}', | ||||||
|             ); |             ); | ||||||
|             // Call setRemoteActivityStatus |             final label = data['args']['activity']['details'] ?? ''; | ||||||
|             final label = data['args']['activity']['details'] ?? 'Unknown'; |  | ||||||
|             final appId = socket.clientId; |             final appId = socket.clientId; | ||||||
|  |             final meta = data['args']['activity']; | ||||||
|             try { |             try { | ||||||
|               await setRemoteActivityStatus(ref, label, appId); |               await setRemoteActivityStatus( | ||||||
|  |                 ref, | ||||||
|  |                 label, | ||||||
|  |                 appId, | ||||||
|  |                 meta, | ||||||
|  |               ); | ||||||
|  |               final now = DateTime.now(); | ||||||
|  |               final status = SnAccountStatus( | ||||||
|  |                 id: 'local_$appId', | ||||||
|  |                 attitude: 0, | ||||||
|  |                 isOnline: true, | ||||||
|  |                 isInvisible: false, | ||||||
|  |                 isNotDisturb: false, | ||||||
|  |                 isCustomized: true, | ||||||
|  |                 label: label, | ||||||
|  |                 meta: meta, | ||||||
|  |                 clearedAt: null, | ||||||
|  |                 accountId: 'me', | ||||||
|  |                 createdAt: now, | ||||||
|  |                 updatedAt: now, | ||||||
|  |                 deletedAt: null, | ||||||
|  |               ); | ||||||
|  |               ref.read(currentAccountStatusProvider.notifier).setStatus(status); | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|               developer.log( |               developer.log( | ||||||
|                 'Failed to set remote activity status: $e', |                 'Failed to set remote activity status: $e', | ||||||
|                 name: kRpcLogPrefix, |                 name: kRpcLogPrefix, | ||||||
|               ); |               ); | ||||||
|             } |             } | ||||||
|             // Echo back success |  | ||||||
|             socket.send({ |             socket.send({ | ||||||
|               'cmd': 'SET_ACTIVITY', |               'cmd': 'SET_ACTIVITY', | ||||||
|               'data': data['args']['activity'], |               'data': data['args']['activity'], | ||||||
| @@ -608,6 +440,7 @@ final rpcServerStateProvider = | |||||||
|           final appId = socket.clientId; |           final appId = socket.clientId; | ||||||
|           try { |           try { | ||||||
|             await unsetRemoteActivityStatus(ref, appId); |             await unsetRemoteActivityStatus(ref, appId); | ||||||
|  |             ref.read(currentAccountStatusProvider.notifier).clearStatus(); | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
|             developer.log( |             developer.log( | ||||||
|               'Failed to unset remote activity status: $e', |               'Failed to unset remote activity status: $e', | ||||||
| @@ -628,6 +461,7 @@ Future<void> setRemoteActivityStatus( | |||||||
|   Ref ref, |   Ref ref, | ||||||
|   String label, |   String label, | ||||||
|   String appId, |   String appId, | ||||||
|  |   Map<String, dynamic> meta, | ||||||
| ) async { | ) async { | ||||||
|   final apiClient = ref.read(apiClientProvider); |   final apiClient = ref.read(apiClientProvider); | ||||||
|   await apiClient.post( |   await apiClient.post( | ||||||
| @@ -638,6 +472,7 @@ Future<void> setRemoteActivityStatus( | |||||||
|       'is_automated': true, |       'is_automated': true, | ||||||
|       'label': label, |       'label': label, | ||||||
|       'app_identifier': appId, |       'app_identifier': appId, | ||||||
|  |       'meta': meta, | ||||||
|     }, |     }, | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
							
								
								
									
										297
									
								
								lib/pods/activity/ipc_server.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								lib/pods/activity/ipc_server.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,297 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'dart:convert'; | ||||||
|  | import 'dart:developer' as developer; | ||||||
|  | import 'dart:io'; | ||||||
|  | import 'dart:typed_data'; | ||||||
|  | import 'package:dart_ipc/dart_ipc.dart'; | ||||||
|  | import 'package:path/path.dart' as path; | ||||||
|  |  | ||||||
|  | const String kRpcIpcLogPrefix = 'arRPC.ipc'; | ||||||
|  |  | ||||||
|  | // IPC Packet Types | ||||||
|  | class IpcTypes { | ||||||
|  |   static const int handshake = 0; | ||||||
|  |   static const int frame = 1; | ||||||
|  |   static const int close = 2; | ||||||
|  |   static const int ping = 3; | ||||||
|  |   static const int pong = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IPC Close Codes | ||||||
|  | class IpcCloseCodes { | ||||||
|  |   static const int closeNormal = 1000; | ||||||
|  |   static const int closeUnsupported = 1003; | ||||||
|  |   static const int closeAbnormal = 1006; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IPC Error Codes | ||||||
|  | class IpcErrorCodes { | ||||||
|  |   static const int invalidClientId = 4000; | ||||||
|  |   static const int invalidOrigin = 4001; | ||||||
|  |   static const int rateLimited = 4002; | ||||||
|  |   static const int tokenRevoked = 4003; | ||||||
|  |   static const int invalidVersion = 4004; | ||||||
|  |   static const int invalidEncoding = 4005; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IPC Packet structure | ||||||
|  | class IpcPacket { | ||||||
|  |   final int type; | ||||||
|  |   final Map<String, dynamic> data; | ||||||
|  |  | ||||||
|  |   IpcPacket(this.type, this.data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Abstract base class for IPC server | ||||||
|  | abstract class IpcServer { | ||||||
|  |   final List<IpcSocketWrapper> _sockets = []; | ||||||
|  |  | ||||||
|  |   // Encode IPC packet | ||||||
|  |   static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) { | ||||||
|  |     final jsonData = jsonEncode(data); | ||||||
|  |     final dataBytes = utf8.encode(jsonData); | ||||||
|  |     final dataSize = dataBytes.length; | ||||||
|  |  | ||||||
|  |     final buffer = ByteData(8 + dataSize); | ||||||
|  |     buffer.setInt32(0, type, Endian.little); | ||||||
|  |     buffer.setInt32(4, dataSize, Endian.little); | ||||||
|  |     buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes); | ||||||
|  |  | ||||||
|  |     return buffer.buffer.asUint8List(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> start(); | ||||||
|  |   Future<void> stop(); | ||||||
|  |  | ||||||
|  |   void addSocket(IpcSocketWrapper socket) { | ||||||
|  |     _sockets.add(socket); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void removeSocket(IpcSocketWrapper socket) { | ||||||
|  |     _sockets.remove(socket); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   List<IpcSocketWrapper> get sockets => _sockets; | ||||||
|  |  | ||||||
|  |   void Function( | ||||||
|  |     IpcSocketWrapper socket, | ||||||
|  |     IpcPacket packet, | ||||||
|  |     Map<String, Function> handlers, | ||||||
|  |   )? | ||||||
|  |   handlePacket; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Abstract base class for IPC socket wrapper | ||||||
|  | abstract class IpcSocketWrapper { | ||||||
|  |   String clientId = ''; | ||||||
|  |   bool handshook = false; | ||||||
|  |   final List<int> _buffer = []; | ||||||
|  |  | ||||||
|  |   void addData(List<int> data) { | ||||||
|  |     _buffer.addAll(data); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void send(Map<String, dynamic> msg); | ||||||
|  |   void sendPong(dynamic data); | ||||||
|  |   void close(); | ||||||
|  |   void closeWithCode(int code, [String message = '']); | ||||||
|  |  | ||||||
|  |   List<IpcPacket> readPackets() { | ||||||
|  |     final packets = <IpcPacket>[]; | ||||||
|  |  | ||||||
|  |     while (_buffer.length >= 8) { | ||||||
|  |       final buffer = Uint8List.fromList(_buffer); | ||||||
|  |       final byteData = ByteData.view(buffer.buffer); | ||||||
|  |  | ||||||
|  |       final type = byteData.getInt32(0, Endian.little); | ||||||
|  |       final dataSize = byteData.getInt32(4, Endian.little); | ||||||
|  |  | ||||||
|  |       if (_buffer.length < 8 + dataSize) break; | ||||||
|  |  | ||||||
|  |       final dataBytes = _buffer.sublist(8, 8 + dataSize); | ||||||
|  |       final jsonStr = utf8.decode(dataBytes); | ||||||
|  |       final jsonData = jsonDecode(jsonStr); | ||||||
|  |  | ||||||
|  |       packets.add(IpcPacket(type, jsonData)); | ||||||
|  |  | ||||||
|  |       _buffer.removeRange(0, 8 + dataSize); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return packets; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Multiplatform IPC Server implementation using dart_ipc | ||||||
|  | class MultiPlatformIpcServer extends IpcServer { | ||||||
|  |   StreamSubscription? _serverSubscription; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<void> start() async { | ||||||
|  |     try { | ||||||
|  |       final ipcPath = Platform.isWindows | ||||||
|  |           ? r'\\.\pipe\discord-ipc-0' | ||||||
|  |           : await _findAvailableUnixIpcPath(); | ||||||
|  |  | ||||||
|  |       final serverSocket = await bind(ipcPath); | ||||||
|  |       developer.log( | ||||||
|  |         'IPC listening at $ipcPath', | ||||||
|  |         name: kRpcIpcLogPrefix, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       _serverSubscription = serverSocket.listen((socket) { | ||||||
|  |         final socketWrapper = MultiPlatformIpcSocketWrapper(socket); | ||||||
|  |         addSocket(socketWrapper); | ||||||
|  |         developer.log( | ||||||
|  |           'New IPC connection!', | ||||||
|  |           name: kRpcIpcLogPrefix, | ||||||
|  |         ); | ||||||
|  |         _handleIpcData(socketWrapper); | ||||||
|  |       }); | ||||||
|  |     } catch (e) { | ||||||
|  |       throw Exception('Failed to start IPC server: $e'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<void> stop() async { | ||||||
|  |     for (var socket in sockets) { | ||||||
|  |       try { | ||||||
|  |         socket.close(); | ||||||
|  |       } catch (e) { | ||||||
|  |         developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     sockets.clear(); | ||||||
|  |     _serverSubscription?.cancel(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Handle incoming IPC data | ||||||
|  |   void _handleIpcData(MultiPlatformIpcSocketWrapper socket) { | ||||||
|  |     final startTime = DateTime.now(); | ||||||
|  |     socket.socket.listen((data) { | ||||||
|  |       final readStart = DateTime.now(); | ||||||
|  |       socket.addData(data); | ||||||
|  |       final readDuration = DateTime.now().difference(readStart).inMicroseconds; | ||||||
|  |       developer.log( | ||||||
|  |         'Read data took $readDuration microseconds', | ||||||
|  |         name: kRpcIpcLogPrefix, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       final packets = socket.readPackets(); | ||||||
|  |       for (final packet in packets) { | ||||||
|  |         handlePacket?.call(socket, packet, {}); | ||||||
|  |       } | ||||||
|  |     }, onDone: () { | ||||||
|  |       developer.log('IPC connection closed', name: kRpcIpcLogPrefix); | ||||||
|  |       socket.close(); | ||||||
|  |     }, onError: (e) { | ||||||
|  |       developer.log('IPC data error: $e', name: kRpcIpcLogPrefix); | ||||||
|  |       socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString()); | ||||||
|  |     }); | ||||||
|  |     final totalDuration = DateTime.now().difference(startTime).inMicroseconds; | ||||||
|  |     developer.log( | ||||||
|  |       '_handleIpcData took $totalDuration microseconds', | ||||||
|  |       name: kRpcIpcLogPrefix, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<String> _getMacOsSystemTmpDir() async { | ||||||
|  |     final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']); | ||||||
|  |     return (result.stdout as String).trim(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Find available IPC socket path for Unix-like systems | ||||||
|  |   Future<String> _findAvailableUnixIpcPath() async { | ||||||
|  |     // Build list of directories to try, with macOS-specific handling | ||||||
|  |     final baseDirs = <String>[]; | ||||||
|  |  | ||||||
|  |     if (Platform.isMacOS) { | ||||||
|  |       try { | ||||||
|  |         final macTempDir = await _getMacOsSystemTmpDir(); | ||||||
|  |         if (macTempDir.isNotEmpty) { | ||||||
|  |           baseDirs.add(macTempDir); | ||||||
|  |         } | ||||||
|  |       } catch (e) { | ||||||
|  |         developer.log( | ||||||
|  |           'Failed to get macOS system temp dir: $e', | ||||||
|  |           name: kRpcIpcLogPrefix, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Add other standard directories | ||||||
|  |     final otherDirs = [ | ||||||
|  |       Platform.environment['XDG_RUNTIME_DIR'], | ||||||
|  |       Platform.environment['TMPDIR'], | ||||||
|  |       Platform.environment['TMP'], | ||||||
|  |       Platform.environment['TEMP'], | ||||||
|  |       '/tmp', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     baseDirs.addAll( | ||||||
|  |       otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     for (final baseDir in baseDirs) { | ||||||
|  |       for (int i = 0; i < 10; i++) { | ||||||
|  |         final socketPath = path.join(baseDir, 'discord-ipc-$i'); | ||||||
|  |         try { | ||||||
|  |           final socket = await bind(socketPath); | ||||||
|  |           socket.close(); | ||||||
|  |           try { | ||||||
|  |             await File(socketPath).delete(); | ||||||
|  |           } catch (_) {} | ||||||
|  |           developer.log( | ||||||
|  |             'IPC socket will be created at: $socketPath', | ||||||
|  |             name: kRpcIpcLogPrefix, | ||||||
|  |           ); | ||||||
|  |           return socketPath; | ||||||
|  |         } catch (e) { | ||||||
|  |           if (i == 0) { | ||||||
|  |             developer.log( | ||||||
|  |               'IPC path $socketPath not available: $e', | ||||||
|  |               name: kRpcIpcLogPrefix, | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     throw Exception( | ||||||
|  |       'No available IPC socket paths found in any temp directory', | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Multiplatform IPC Socket Wrapper | ||||||
|  | class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper { | ||||||
|  |   final dynamic socket; | ||||||
|  |  | ||||||
|  |   MultiPlatformIpcSocketWrapper(this.socket); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void send(Map<String, dynamic> msg) { | ||||||
|  |     developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix); | ||||||
|  |     final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg); | ||||||
|  |     socket.add(packet); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void sendPong(dynamic data) { | ||||||
|  |     final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {}); | ||||||
|  |     socket.add(packet); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void close() { | ||||||
|  |     socket.close(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void closeWithCode(int code, [String message = '']) { | ||||||
|  |     final closeData = {'code': code, 'message': message}; | ||||||
|  |     final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData); | ||||||
|  |     socket.add(packet); | ||||||
|  |     socket.close(); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								lib/pods/activity/ipc_server.web.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								lib/pods/activity/ipc_server.web.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | // Stub implementation for web platform | ||||||
|  | // This file provides empty implementations to avoid import errors on web | ||||||
|  |  | ||||||
|  | // IPC Packet Types | ||||||
|  | class IpcTypes { | ||||||
|  |   static const int handshake = 0; | ||||||
|  |   static const int frame = 1; | ||||||
|  |   static const int close = 2; | ||||||
|  |   static const int ping = 3; | ||||||
|  |   static const int pong = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IPC Close Codes | ||||||
|  | class IpcCloseCodes { | ||||||
|  |   static const int closeNormal = 1000; | ||||||
|  |   static const int closeUnsupported = 1003; | ||||||
|  |   static const int closeAbnormal = 1006; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IPC Error Codes | ||||||
|  | class IpcErrorCodes { | ||||||
|  |   static const int invalidClientId = 4000; | ||||||
|  |   static const int invalidOrigin = 4001; | ||||||
|  |   static const int rateLimited = 4002; | ||||||
|  |   static const int tokenRevoked = 4003; | ||||||
|  |   static const int invalidVersion = 4004; | ||||||
|  |   static const int invalidEncoding = 4005; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IPC Packet structure | ||||||
|  | class IpcPacket { | ||||||
|  |   final int type; | ||||||
|  |   final Map<String, dynamic> data; | ||||||
|  |  | ||||||
|  |   IpcPacket(this.type, this.data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class IpcServer { | ||||||
|  |   Future<void> start() async {} | ||||||
|  |   Future<void> stop() async {} | ||||||
|  |   void Function(dynamic, dynamic, dynamic)? handlePacket; | ||||||
|  |   void addSocket(dynamic socket) {} | ||||||
|  |   void removeSocket(dynamic socket) {} | ||||||
|  |   List<dynamic> get sockets => []; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class IpcSocketWrapper { | ||||||
|  |   String clientId = ''; | ||||||
|  |   bool handshook = false; | ||||||
|  |  | ||||||
|  |   void addData(List<int> data) {} | ||||||
|  |   void send(Map<String, dynamic> msg) {} | ||||||
|  |   void sendPong(dynamic data) {} | ||||||
|  |   void close() {} | ||||||
|  |   void closeWithCode(int code, [String message = '']) {} | ||||||
|  |   List<dynamic> readPackets() => []; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class MultiPlatformIpcServer extends IpcServer {} | ||||||
|  |  | ||||||
|  | class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {} | ||||||
							
								
								
									
										34
									
								
								lib/pods/chat_rooms.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/pods/chat_rooms.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | import "dart:async"; | ||||||
|  | import "package:flutter/material.dart"; | ||||||
|  | import "package:hooks_riverpod/hooks_riverpod.dart"; | ||||||
|  |  | ||||||
|  | final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false); | ||||||
|  |  | ||||||
|  | final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {}); | ||||||
|  |  | ||||||
|  | final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) { | ||||||
|  |   final controller = StreamController<AppLifecycleState>(); | ||||||
|  |  | ||||||
|  |   final observer = _AppLifecycleObserver((state) { | ||||||
|  |     if (controller.isClosed) return; | ||||||
|  |     controller.add(state); | ||||||
|  |   }); | ||||||
|  |   WidgetsBinding.instance.addObserver(observer); | ||||||
|  |  | ||||||
|  |   ref.onDispose(() { | ||||||
|  |     WidgetsBinding.instance.removeObserver(observer); | ||||||
|  |     controller.close(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return controller.stream; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | class _AppLifecycleObserver extends WidgetsBindingObserver { | ||||||
|  |   final ValueChanged<AppLifecycleState> onChange; | ||||||
|  |   _AppLifecycleObserver(this.onChange); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||||
|  |     onChange(state); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -25,6 +25,8 @@ const kAppSoundEffects = 'app_sound_effects'; | |||||||
| const kAppAprilFoolFeatures = 'app_april_fool_features'; | const kAppAprilFoolFeatures = 'app_april_fool_features'; | ||||||
| const kAppWindowSize = 'app_window_size'; | const kAppWindowSize = 'app_window_size'; | ||||||
| const kAppEnterToSend = 'app_enter_to_send'; | const kAppEnterToSend = 'app_enter_to_send'; | ||||||
|  | const kAppDefaultPoolId = 'app_default_pool_id'; | ||||||
|  | const kAppMessageDisplayStyle = 'app_message_display_style'; | ||||||
| const kFeaturedPostsCollapsedId = | const kFeaturedPostsCollapsedId = | ||||||
|     'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post |     'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post | ||||||
|  |  | ||||||
| @@ -65,6 +67,8 @@ sealed class AppSettings with _$AppSettings { | |||||||
|     required String? customFonts, |     required String? customFonts, | ||||||
|     required int? appColorScheme, // The color stored via the int type |     required int? appColorScheme, // The color stored via the int type | ||||||
|     required Size? windowSize, // The window size for desktop platforms |     required Size? windowSize, // The window size for desktop platforms | ||||||
|  |     required String? defaultPoolId, | ||||||
|  |     required String messageDisplayStyle, | ||||||
|   }) = _AppSettings; |   }) = _AppSettings; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -84,6 +88,8 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | |||||||
|       customFonts: prefs.getString(kAppCustomFonts), |       customFonts: prefs.getString(kAppCustomFonts), | ||||||
|       appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), |       appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), | ||||||
|       windowSize: _getWindowSizeFromPrefs(prefs), |       windowSize: _getWindowSizeFromPrefs(prefs), | ||||||
|  |       defaultPoolId: prefs.getString(kAppDefaultPoolId), | ||||||
|  |       messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble', | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -104,6 +110,16 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | |||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void setDefaultPoolId(String? value) { | ||||||
|  |     final prefs = ref.read(sharedPreferencesProvider); | ||||||
|  |     if (value != null) { | ||||||
|  |       prefs.setString(kAppDefaultPoolId, value); | ||||||
|  |     } else { | ||||||
|  |       prefs.remove(kAppDefaultPoolId); | ||||||
|  |     } | ||||||
|  |     state = state.copyWith(defaultPoolId: value); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void setAutoTranslate(bool value) { |   void setAutoTranslate(bool value) { | ||||||
|     final prefs = ref.read(sharedPreferencesProvider); |     final prefs = ref.read(sharedPreferencesProvider); | ||||||
|     prefs.setBool(kAppAutoTranslate, value); |     prefs.setBool(kAppAutoTranslate, value); | ||||||
| @@ -174,6 +190,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | |||||||
|   Size? getWindowSize() { |   Size? getWindowSize() { | ||||||
|     return state.windowSize; |     return state.windowSize; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void setMessageDisplayStyle(String value) { | ||||||
|  |     final prefs = ref.read(sharedPreferencesProvider); | ||||||
|  |     prefs.setString(kAppMessageDisplayStyle, value); | ||||||
|  |     state = state.copyWith(messageDisplayStyle: value); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| final updateInfoProvider = | final updateInfoProvider = | ||||||
|   | |||||||
| @@ -15,7 +15,8 @@ T _$identity<T>(T value) => value; | |||||||
| mixin _$AppSettings { | mixin _$AppSettings { | ||||||
|  |  | ||||||
|  bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type |  bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type | ||||||
|  Size? get windowSize; |  Size? get windowSize;// The window size for desktop platforms | ||||||
|  |  String? get defaultPoolId; String get messageDisplayStyle; | ||||||
| /// Create a copy of AppSettings | /// Create a copy of AppSettings | ||||||
| /// 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) | ||||||
| @@ -26,16 +27,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; |   return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -46,7 +47,7 @@ abstract mixin class $AppSettingsCopyWith<$Res>  { | |||||||
|   factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; |   factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize |  bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -63,7 +64,7 @@ class _$AppSettingsCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of AppSettings | /// Create a copy of AppSettings | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable | autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable | as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -75,7 +76,9 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI | |||||||
| as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable | as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable | as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable | ||||||
| as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable | as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable | ||||||
| as Size?, | as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -157,10 +160,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate,  bool dataSavingMode,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate,  bool dataSavingMode,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize,  String? defaultPoolId,  String messageDisplayStyle)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _AppSettings() when $default != null: | case _AppSettings() when $default != null: | ||||||
| return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: | return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -178,10 +181,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate,  bool dataSavingMode,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate,  bool dataSavingMode,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize,  String? defaultPoolId,  String messageDisplayStyle)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _AppSettings(): | case _AppSettings(): | ||||||
| return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);} | return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -195,10 +198,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate,  bool dataSavingMode,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate,  bool dataSavingMode,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize,  String? defaultPoolId,  String messageDisplayStyle)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _AppSettings() when $default != null: | case _AppSettings() when $default != null: | ||||||
| return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: | return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -210,7 +213,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha | |||||||
|  |  | ||||||
|  |  | ||||||
| class _AppSettings implements AppSettings { | class _AppSettings implements AppSettings { | ||||||
|   const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize}); |   const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.defaultPoolId, required this.messageDisplayStyle}); | ||||||
|    |    | ||||||
|  |  | ||||||
| @override final  bool autoTranslate; | @override final  bool autoTranslate; | ||||||
| @@ -224,6 +227,9 @@ class _AppSettings implements AppSettings { | |||||||
| @override final  int? appColorScheme; | @override final  int? appColorScheme; | ||||||
| // The color stored via the int type | // The color stored via the int type | ||||||
| @override final  Size? windowSize; | @override final  Size? windowSize; | ||||||
|  | // The window size for desktop platforms | ||||||
|  | @override final  String? defaultPoolId; | ||||||
|  | @override final  String messageDisplayStyle; | ||||||
|  |  | ||||||
| /// Create a copy of AppSettings | /// Create a copy of AppSettings | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @@ -235,16 +241,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_ | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; |   return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -255,7 +261,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith | |||||||
|   factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; |   factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize |  bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -272,7 +278,7 @@ class __$AppSettingsCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of AppSettings | /// Create a copy of AppSettings | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) { | ||||||
|   return _then(_AppSettings( |   return _then(_AppSettings( | ||||||
| autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable | autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable | as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -284,7 +290,9 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI | |||||||
| as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable | as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable | as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable | ||||||
| as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable | as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable | ||||||
| as Size?, | as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ part of 'config.dart'; | |||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$appSettingsNotifierHash() => | String _$appSettingsNotifierHash() => | ||||||
|     r'cd18bff2614a94e3523634e6c577cefad0367eba'; |     r'9f0979f18b107e61185391e7c39bd81ac4b8ca50'; | ||||||
|  |  | ||||||
| /// See also [AppSettingsNotifier]. | /// See also [AppSettingsNotifier]. | ||||||
| @ProviderFor(AppSettingsNotifier) | @ProviderFor(AppSettingsNotifier) | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								lib/pods/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/pods/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/file_pool.dart'; | ||||||
|  | import 'package:island/pods/config.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  |  | ||||||
|  | final poolsProvider = FutureProvider<List<SnFilePool>>((ref) async { | ||||||
|  |   final dio = ref.watch(apiClientProvider); | ||||||
|  |   final response = await dio.get('/drive/pools'); | ||||||
|  |   return response.data | ||||||
|  |       .map((e) => SnFilePool.fromJson(e)) | ||||||
|  |       .cast<SnFilePool>() | ||||||
|  |       .toList(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | String? resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) { | ||||||
|  |   final settings = ref.watch(appSettingsNotifierProvider); | ||||||
|  |  | ||||||
|  |   final configuredId = settings.defaultPoolId; | ||||||
|  |   if (configuredId != null && pools.any((p) => p.id == configuredId)) { | ||||||
|  |     return configuredId; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return pools.firstOrNull?.id; | ||||||
|  | } | ||||||
| @@ -10,17 +10,19 @@ Future<void> resetDatabase(WidgetRef ref) async { | |||||||
|   if (kIsWeb) return; |   if (kIsWeb) return; | ||||||
|  |  | ||||||
|   final db = ref.read(databaseProvider); |   final db = ref.read(databaseProvider); | ||||||
|   final basepath = await getApplicationSupportDirectory(); |  | ||||||
|   final file = File(join(basepath.path, 'solar_network_data.sqlite')); |  | ||||||
|  |  | ||||||
|   // Close current database connection |   // Close current database connection | ||||||
|   db.close(); |   await db.close(); | ||||||
|  |  | ||||||
|   // Delete database file |   // Get the correct database file path | ||||||
|  |   final dbFolder = await getApplicationDocumentsDirectory(); | ||||||
|  |   final file = File(join(dbFolder.path, 'solar_network_data.sqlite')); | ||||||
|  |  | ||||||
|  |   // Delete database file if it exists | ||||||
|   if (await file.exists()) { |   if (await file.exists()) { | ||||||
|     await file.delete(); |     await file.delete(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Force refresh the database provider |   // Force refresh the database provider to create a new instance | ||||||
|   ref.invalidate(databaseProvider); |   ref.invalidate(databaseProvider); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										852
									
								
								lib/pods/messages_notifier.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										852
									
								
								lib/pods/messages_notifier.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,852 @@ | |||||||
|  | import "dart:async"; | ||||||
|  | import "dart:developer" as developer; | ||||||
|  | import "package:dio/dio.dart"; | ||||||
|  | import "package:drift/drift.dart" show Variable; | ||||||
|  | import "package:easy_localization/easy_localization.dart"; | ||||||
|  | import "package:flutter/material.dart"; | ||||||
|  | import "package:island/database/drift_db.dart"; | ||||||
|  | import "package:island/database/message.dart"; | ||||||
|  | import "package:island/models/chat.dart"; | ||||||
|  | import "package:island/models/file.dart"; | ||||||
|  | import "package:island/pods/config.dart"; | ||||||
|  | import "package:island/pods/database.dart"; | ||||||
|  | import "package:island/pods/network.dart"; | ||||||
|  | import "package:island/services/file.dart"; | ||||||
|  | import "package:island/widgets/alert.dart"; | ||||||
|  | import "package:riverpod_annotation/riverpod_annotation.dart"; | ||||||
|  | import "package:uuid/uuid.dart"; | ||||||
|  | import "package:island/screens/chat/chat.dart"; | ||||||
|  | import "package:island/pods/chat_rooms.dart"; | ||||||
|  |  | ||||||
|  | part 'messages_notifier.g.dart'; | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | class MessagesNotifier extends _$MessagesNotifier { | ||||||
|  |   late final Dio _apiClient; | ||||||
|  |   late final AppDatabase _database; | ||||||
|  |   late final SnChatRoom _room; | ||||||
|  |   late final SnChatMember _identity; | ||||||
|  |  | ||||||
|  |   final Map<String, LocalChatMessage> _pendingMessages = {}; | ||||||
|  |   final Map<String, Map<int, double>> _fileUploadProgress = {}; | ||||||
|  |   int? _totalCount; | ||||||
|  |   String? _searchQuery; | ||||||
|  |   bool? _withLinks; | ||||||
|  |   bool? _withAttachments; | ||||||
|  |  | ||||||
|  |   late final String _roomId; | ||||||
|  |   static const int _pageSize = 20; | ||||||
|  |   bool _hasMore = true; | ||||||
|  |   bool _isSyncing = false; | ||||||
|  |   bool _isJumping = false; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   FutureOr<List<LocalChatMessage>> build(String roomId) async { | ||||||
|  |     _roomId = roomId; | ||||||
|  |     _apiClient = ref.watch(apiClientProvider); | ||||||
|  |     _database = ref.watch(databaseProvider); | ||||||
|  |     final room = await ref.watch(chatroomProvider(roomId).future); | ||||||
|  |     final identity = await ref.watch(chatroomIdentityProvider(roomId).future); | ||||||
|  |  | ||||||
|  |     if (room == null) { | ||||||
|  |       throw Exception('Room not found'); | ||||||
|  |     } | ||||||
|  |     _room = room; | ||||||
|  |  | ||||||
|  |     // Allow building even if identity is null for public rooms | ||||||
|  |     if (identity != null) { | ||||||
|  |       _identity = identity; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     developer.log( | ||||||
|  |       'MessagesNotifier built for room $roomId', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Only setup sync and lifecycle listeners if user is a member | ||||||
|  |     if (identity != null) { | ||||||
|  |       ref.listen(appLifecycleStateProvider, (_, next) { | ||||||
|  |         if (next.hasValue && next.value == AppLifecycleState.resumed) { | ||||||
|  |           developer.log( | ||||||
|  |             'App resumed, syncing messages', | ||||||
|  |             name: 'MessagesNotifier', | ||||||
|  |           ); | ||||||
|  |           syncMessages(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     loadInitial(); | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) { | ||||||
|  |     messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); | ||||||
|  |     return messages; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<List<LocalChatMessage>> _getCachedMessages({ | ||||||
|  |     int offset = 0, | ||||||
|  |     int take = 20, | ||||||
|  |   }) async { | ||||||
|  |     developer.log( | ||||||
|  |       'Getting cached messages from offset $offset, take $take', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |     final List<LocalChatMessage> dbMessages; | ||||||
|  |     if (_searchQuery != null && _searchQuery!.isNotEmpty) { | ||||||
|  |       dbMessages = await _database.searchMessages( | ||||||
|  |         _roomId, | ||||||
|  |         _searchQuery ?? '', | ||||||
|  |         withAttachments: _withAttachments, | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       final chatMessagesFromDb = await _database.getMessagesForRoom( | ||||||
|  |         _roomId, | ||||||
|  |         offset: offset, | ||||||
|  |         limit: take, | ||||||
|  |       ); | ||||||
|  |       dbMessages = | ||||||
|  |           chatMessagesFromDb.map(_database.companionToMessage).toList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     List<LocalChatMessage> filteredMessages = dbMessages; | ||||||
|  |  | ||||||
|  |     if (_withLinks == true) { | ||||||
|  |       filteredMessages = | ||||||
|  |           filteredMessages.where((msg) => _hasLink(msg)).toList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final dbLocalMessages = filteredMessages; | ||||||
|  |  | ||||||
|  |     // Always ensure unique messages to prevent duplicate keys | ||||||
|  |     final uniqueMessages = <LocalChatMessage>[]; | ||||||
|  |     final seenIds = <String>{}; | ||||||
|  |     for (final message in dbLocalMessages) { | ||||||
|  |       if (seenIds.add(message.id)) { | ||||||
|  |         uniqueMessages.add(message); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (offset == 0) { | ||||||
|  |       final pendingForRoom = | ||||||
|  |           _pendingMessages.values | ||||||
|  |               .where((msg) => msg.roomId == _roomId) | ||||||
|  |               .toList(); | ||||||
|  |  | ||||||
|  |       final allMessages = [...pendingForRoom, ...uniqueMessages]; | ||||||
|  |       _sortMessages(allMessages); // Use the helper function | ||||||
|  |  | ||||||
|  |       final finalUniqueMessages = <LocalChatMessage>[]; | ||||||
|  |       final finalSeenIds = <String>{}; | ||||||
|  |       for (final message in allMessages) { | ||||||
|  |         if (finalSeenIds.add(message.id)) { | ||||||
|  |           finalUniqueMessages.add(message); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return finalUniqueMessages; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return uniqueMessages; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<List<LocalChatMessage>> _fetchAndCacheMessages({ | ||||||
|  |     int offset = 0, | ||||||
|  |     int take = 20, | ||||||
|  |   }) async { | ||||||
|  |     developer.log( | ||||||
|  |       'Fetching messages from API, offset $offset, take $take', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |     if (_totalCount == null) { | ||||||
|  |       final response = await _apiClient.get( | ||||||
|  |         '/sphere/chat/$_roomId/messages', | ||||||
|  |         queryParameters: {'offset': 0, 'take': 1}, | ||||||
|  |       ); | ||||||
|  |       _totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (offset >= _totalCount!) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final response = await _apiClient.get( | ||||||
|  |       '/sphere/chat/$_roomId/messages', | ||||||
|  |       queryParameters: {'offset': offset, 'take': take}, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final List<dynamic> data = response.data; | ||||||
|  |     _totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0'); | ||||||
|  |  | ||||||
|  |     final messages = | ||||||
|  |         data.map((json) { | ||||||
|  |           final remoteMessage = SnChatMessage.fromJson(json); | ||||||
|  |           return LocalChatMessage.fromRemoteMessage( | ||||||
|  |             remoteMessage, | ||||||
|  |             MessageStatus.sent, | ||||||
|  |           ); | ||||||
|  |         }).toList(); | ||||||
|  |  | ||||||
|  |     for (final message in messages) { | ||||||
|  |       await _database.saveMessage(_database.messageToCompanion(message)); | ||||||
|  |       if (message.nonce != null) { | ||||||
|  |         _pendingMessages.removeWhere( | ||||||
|  |           (_, pendingMsg) => pendingMsg.nonce == message.nonce, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return messages; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> syncMessages() async { | ||||||
|  |     if (_isSyncing) { | ||||||
|  |       developer.log( | ||||||
|  |         'Sync already in progress, skipping.', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     _isSyncing = true; | ||||||
|  |  | ||||||
|  |     developer.log('Starting message sync', name: 'MessagesNotifier'); | ||||||
|  |     Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true); | ||||||
|  |     try { | ||||||
|  |       final dbMessages = await _database.getMessagesForRoom( | ||||||
|  |         _room.id, | ||||||
|  |         offset: 0, | ||||||
|  |         limit: 1, | ||||||
|  |       ); | ||||||
|  |       final lastMessage = | ||||||
|  |           dbMessages.isEmpty | ||||||
|  |               ? null | ||||||
|  |               : _database.companionToMessage(dbMessages.first); | ||||||
|  |  | ||||||
|  |       if (lastMessage == null) { | ||||||
|  |         developer.log( | ||||||
|  |           'No local messages, fetching from network', | ||||||
|  |           name: 'MessagesNotifier', | ||||||
|  |         ); | ||||||
|  |         final newMessages = await _fetchAndCacheMessages( | ||||||
|  |           offset: 0, | ||||||
|  |           take: _pageSize, | ||||||
|  |         ); | ||||||
|  |         state = AsyncValue.data(newMessages); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       final resp = await _apiClient.post( | ||||||
|  |         '/sphere/chat/${_room.id}/sync', | ||||||
|  |         data: { | ||||||
|  |           'last_sync_timestamp': | ||||||
|  |               lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       final response = MessageSyncResponse.fromJson(resp.data); | ||||||
|  |       developer.log( | ||||||
|  |         'Sync response: ${response.messages.length} changes', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |       for (final message in response.messages) { | ||||||
|  |         switch (message.type) { | ||||||
|  |           case "messages.update": | ||||||
|  |           case "messages.update.links": | ||||||
|  |             await receiveMessageUpdate(message); | ||||||
|  |             break; | ||||||
|  |           case "messages.delete": | ||||||
|  |             await receiveMessageDeletion(message.id.toString()); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         // Still need receive the message to show the history actions | ||||||
|  |         await receiveMessage(message); | ||||||
|  |       } | ||||||
|  |     } catch (err, stackTrace) { | ||||||
|  |       developer.log( | ||||||
|  |         'Error syncing messages', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |         error: err, | ||||||
|  |         stackTrace: stackTrace, | ||||||
|  |       ); | ||||||
|  |       showErrorAlert(err); | ||||||
|  |     } finally { | ||||||
|  |       developer.log('Finished message sync', name: 'MessagesNotifier'); | ||||||
|  |       Future.microtask( | ||||||
|  |         () => ref.read(isSyncingProvider.notifier).state = false, | ||||||
|  |       ); | ||||||
|  |       _isSyncing = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<List<LocalChatMessage>> listMessages({ | ||||||
|  |     int offset = 0, | ||||||
|  |     int take = 20, | ||||||
|  |     bool synced = false, | ||||||
|  |   }) async { | ||||||
|  |     try { | ||||||
|  |       if (offset == 0 && | ||||||
|  |           !synced && | ||||||
|  |           (_searchQuery == null || _searchQuery!.isEmpty)) { | ||||||
|  |         _fetchAndCacheMessages(offset: 0, take: take).catchError((_) { | ||||||
|  |           return <LocalChatMessage>[]; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       final localMessages = await _getCachedMessages( | ||||||
|  |         offset: offset, | ||||||
|  |         take: take, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (localMessages.isNotEmpty) { | ||||||
|  |         return localMessages; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (_searchQuery == null || _searchQuery!.isEmpty) { | ||||||
|  |         return await _fetchAndCacheMessages(offset: offset, take: take); | ||||||
|  |       } else { | ||||||
|  |         return []; // If searching, and no local messages, don't fetch from network | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       final localMessages = await _getCachedMessages( | ||||||
|  |         offset: offset, | ||||||
|  |         take: take, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (localMessages.isNotEmpty) { | ||||||
|  |         return localMessages; | ||||||
|  |       } | ||||||
|  |       rethrow; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> loadInitial() async { | ||||||
|  |     developer.log('Loading initial messages', name: 'MessagesNotifier'); | ||||||
|  |     if (_searchQuery == null || _searchQuery!.isEmpty) { | ||||||
|  |       syncMessages(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final messages = await _getCachedMessages(offset: 0, take: _pageSize); | ||||||
|  |  | ||||||
|  |     _hasMore = messages.length == _pageSize; | ||||||
|  |  | ||||||
|  |     state = AsyncValue.data(messages); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> loadMore() async { | ||||||
|  |     if (!_hasMore || state is AsyncLoading) return; | ||||||
|  |     developer.log('Loading more messages', name: 'MessagesNotifier'); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       final currentMessages = state.value ?? []; | ||||||
|  |       final offset = currentMessages.length; | ||||||
|  |  | ||||||
|  |       final newMessages = await listMessages(offset: offset, take: _pageSize); | ||||||
|  |  | ||||||
|  |       if (newMessages.isEmpty || newMessages.length < _pageSize) { | ||||||
|  |         _hasMore = false; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       state = AsyncValue.data( | ||||||
|  |         _sortMessages([...currentMessages, ...newMessages]), | ||||||
|  |       ); | ||||||
|  |     } catch (err, stackTrace) { | ||||||
|  |       developer.log( | ||||||
|  |         'Error loading more messages', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |         error: err, | ||||||
|  |         stackTrace: stackTrace, | ||||||
|  |       ); | ||||||
|  |       showErrorAlert(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> sendMessage( | ||||||
|  |     String content, | ||||||
|  |     List<UniversalFile> attachments, { | ||||||
|  |     SnChatMessage? editingTo, | ||||||
|  |     SnChatMessage? forwardingTo, | ||||||
|  |     SnChatMessage? replyingTo, | ||||||
|  |     Function(String, Map<int, double>)? onProgress, | ||||||
|  |   }) async { | ||||||
|  |     final nonce = const Uuid().v4(); | ||||||
|  |     developer.log( | ||||||
|  |       'Sending message with nonce $nonce', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |     final baseUrl = ref.read(serverUrlProvider); | ||||||
|  |     final token = await getToken(ref.watch(tokenProvider)); | ||||||
|  |     if (token == null) throw ArgumentError('Access token is null'); | ||||||
|  |  | ||||||
|  |     final mockMessage = SnChatMessage( | ||||||
|  |       id: 'pending_$nonce', | ||||||
|  |       chatRoomId: _roomId, | ||||||
|  |       senderId: _identity.id, | ||||||
|  |       content: content, | ||||||
|  |       createdAt: DateTime.now(), | ||||||
|  |       updatedAt: DateTime.now(), | ||||||
|  |       nonce: nonce, | ||||||
|  |       sender: _identity, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final localMessage = LocalChatMessage.fromRemoteMessage( | ||||||
|  |       mockMessage, | ||||||
|  |       MessageStatus.pending, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     _pendingMessages[localMessage.id] = localMessage; | ||||||
|  |     _fileUploadProgress[localMessage.id] = {}; | ||||||
|  |     await _database.saveMessage(_database.messageToCompanion(localMessage)); | ||||||
|  |  | ||||||
|  |     final currentMessages = state.value ?? []; | ||||||
|  |     state = AsyncValue.data([localMessage, ...currentMessages]); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       var cloudAttachments = List.empty(growable: true); | ||||||
|  |       for (var idx = 0; idx < attachments.length; idx++) { | ||||||
|  |         final cloudFile = | ||||||
|  |             await putFileToCloud( | ||||||
|  |               fileData: attachments[idx], | ||||||
|  |               atk: token, | ||||||
|  |               baseUrl: baseUrl, | ||||||
|  |               filename: attachments[idx].data.name ?? 'Post media', | ||||||
|  |               mimetype: | ||||||
|  |                   attachments[idx].data.mimeType ?? | ||||||
|  |                   switch (attachments[idx].type) { | ||||||
|  |                     UniversalFileType.image => 'image/unknown', | ||||||
|  |                     UniversalFileType.video => 'video/unknown', | ||||||
|  |                     UniversalFileType.audio => 'audio/unknown', | ||||||
|  |                     UniversalFileType.file => 'application/octet-stream', | ||||||
|  |                   }, | ||||||
|  |               onProgress: (progress, _) { | ||||||
|  |                 _fileUploadProgress[localMessage.id]?[idx] = progress; | ||||||
|  |                 onProgress?.call( | ||||||
|  |                   localMessage.id, | ||||||
|  |                   _fileUploadProgress[localMessage.id] ?? {}, | ||||||
|  |                 ); | ||||||
|  |               }, | ||||||
|  |             ).future; | ||||||
|  |         if (cloudFile == null) { | ||||||
|  |           throw ArgumentError('Failed to upload the file...'); | ||||||
|  |         } | ||||||
|  |         cloudAttachments.add(cloudFile); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       final response = await _apiClient.request( | ||||||
|  |         editingTo == null | ||||||
|  |             ? '/sphere/chat/$_roomId/messages' | ||||||
|  |             : '/sphere/chat/$_roomId/messages/${editingTo.id}', | ||||||
|  |         data: { | ||||||
|  |           'content': content, | ||||||
|  |           'attachments_id': cloudAttachments.map((e) => e.id).toList(), | ||||||
|  |           'replied_message_id': replyingTo?.id, | ||||||
|  |           'forwarded_message_id': forwardingTo?.id, | ||||||
|  |           'meta': {}, | ||||||
|  |           'nonce': nonce, | ||||||
|  |         }, | ||||||
|  |         options: Options(method: editingTo == null ? 'POST' : 'PATCH'), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       final remoteMessage = SnChatMessage.fromJson(response.data); | ||||||
|  |       final updatedMessage = LocalChatMessage.fromRemoteMessage( | ||||||
|  |         remoteMessage, | ||||||
|  |         MessageStatus.sent, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       _pendingMessages.remove(localMessage.id); | ||||||
|  |       await _database.deleteMessage(localMessage.id); | ||||||
|  |       await _database.saveMessage(_database.messageToCompanion(updatedMessage)); | ||||||
|  |  | ||||||
|  |       final currentMessages = state.value ?? []; | ||||||
|  |       if (editingTo != null) { | ||||||
|  |         final newMessages = | ||||||
|  |             currentMessages | ||||||
|  |                 .where((m) => m.id != localMessage.id) // remove pending message | ||||||
|  |                 .map( | ||||||
|  |                   (m) => m.id == editingTo.id ? updatedMessage : m, | ||||||
|  |                 ) // update original message | ||||||
|  |                 .toList(); | ||||||
|  |         state = AsyncValue.data(newMessages); | ||||||
|  |       } else { | ||||||
|  |         final newMessages = | ||||||
|  |             currentMessages.map((m) { | ||||||
|  |               if (m.id == localMessage.id) { | ||||||
|  |                 return updatedMessage; | ||||||
|  |               } | ||||||
|  |               return m; | ||||||
|  |             }).toList(); | ||||||
|  |         state = AsyncValue.data(newMessages); | ||||||
|  |       } | ||||||
|  |       developer.log( | ||||||
|  |         'Message with nonce $nonce sent successfully', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |     } catch (e, stackTrace) { | ||||||
|  |       developer.log( | ||||||
|  |         'Failed to send message with nonce $nonce', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |         error: e, | ||||||
|  |         stackTrace: stackTrace, | ||||||
|  |       ); | ||||||
|  |       localMessage.status = MessageStatus.failed; | ||||||
|  |       _pendingMessages[localMessage.id] = localMessage; | ||||||
|  |       await _database.updateMessageStatus( | ||||||
|  |         localMessage.id, | ||||||
|  |         MessageStatus.failed, | ||||||
|  |       ); | ||||||
|  |       final newMessages = | ||||||
|  |           (state.value ?? []).map((m) { | ||||||
|  |             if (m.id == localMessage.id) { | ||||||
|  |               return m..status = MessageStatus.failed; | ||||||
|  |             } | ||||||
|  |             return m; | ||||||
|  |           }).toList(); | ||||||
|  |       state = AsyncValue.data(newMessages); | ||||||
|  |       showErrorAlert(e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> retryMessage(String pendingMessageId) async { | ||||||
|  |     developer.log( | ||||||
|  |       'Retrying message $pendingMessageId', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |     final message = await fetchMessageById(pendingMessageId); | ||||||
|  |     if (message == null) { | ||||||
|  |       throw Exception('Message not found'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     message.status = MessageStatus.pending; | ||||||
|  |     _pendingMessages[pendingMessageId] = message; | ||||||
|  |     await _database.updateMessageStatus( | ||||||
|  |       pendingMessageId, | ||||||
|  |       MessageStatus.pending, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       var remoteMessage = message.toRemoteMessage(); | ||||||
|  |       final response = await _apiClient.post( | ||||||
|  |         '/sphere/chat/${message.roomId}/messages', | ||||||
|  |         data: { | ||||||
|  |           'content': remoteMessage.content, | ||||||
|  |           'attachments_id': remoteMessage.attachments, | ||||||
|  |           'meta': remoteMessage.meta, | ||||||
|  |           'nonce': message.nonce, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       remoteMessage = SnChatMessage.fromJson(response.data); | ||||||
|  |       final updatedMessage = LocalChatMessage.fromRemoteMessage( | ||||||
|  |         remoteMessage, | ||||||
|  |         MessageStatus.sent, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       _pendingMessages.remove(pendingMessageId); | ||||||
|  |       await _database.deleteMessage(pendingMessageId); | ||||||
|  |       await _database.saveMessage(_database.messageToCompanion(updatedMessage)); | ||||||
|  |  | ||||||
|  |       final newMessages = | ||||||
|  |           (state.value ?? []).map((m) { | ||||||
|  |             if (m.id == pendingMessageId) { | ||||||
|  |               return updatedMessage; | ||||||
|  |             } | ||||||
|  |             return m; | ||||||
|  |           }).toList(); | ||||||
|  |       state = AsyncValue.data(newMessages); | ||||||
|  |     } catch (e, stackTrace) { | ||||||
|  |       developer.log( | ||||||
|  |         'Failed to retry message $pendingMessageId', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |         error: e, | ||||||
|  |         stackTrace: stackTrace, | ||||||
|  |       ); | ||||||
|  |       message.status = MessageStatus.failed; | ||||||
|  |       _pendingMessages[pendingMessageId] = message; | ||||||
|  |       await _database.updateMessageStatus( | ||||||
|  |         pendingMessageId, | ||||||
|  |         MessageStatus.failed, | ||||||
|  |       ); | ||||||
|  |       final newMessages = | ||||||
|  |           (state.value ?? []).map((m) { | ||||||
|  |             if (m.id == pendingMessageId) { | ||||||
|  |               return m..status = MessageStatus.failed; | ||||||
|  |             } | ||||||
|  |             return m; | ||||||
|  |           }).toList(); | ||||||
|  |       state = AsyncValue.data(_sortMessages(newMessages)); | ||||||
|  |       showErrorAlert(e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> receiveMessage(SnChatMessage remoteMessage) async { | ||||||
|  |     if (remoteMessage.chatRoomId != _roomId) return; | ||||||
|  |     developer.log( | ||||||
|  |       'Received new message ${remoteMessage.id}', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final localMessage = LocalChatMessage.fromRemoteMessage( | ||||||
|  |       remoteMessage, | ||||||
|  |       MessageStatus.sent, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (remoteMessage.nonce != null) { | ||||||
|  |       _pendingMessages.removeWhere( | ||||||
|  |         (_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await _database.saveMessage(_database.messageToCompanion(localMessage)); | ||||||
|  |  | ||||||
|  |     final currentMessages = state.value ?? []; | ||||||
|  |     final existingIndex = currentMessages.indexWhere( | ||||||
|  |       (m) => | ||||||
|  |           m.id == localMessage.id || | ||||||
|  |           (localMessage.nonce != null && m.nonce == localMessage.nonce), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (existingIndex >= 0) { | ||||||
|  |       final newList = [...currentMessages]; | ||||||
|  |       newList[existingIndex] = localMessage; | ||||||
|  |       state = AsyncValue.data(_sortMessages(newList)); | ||||||
|  |     } else { | ||||||
|  |       state = AsyncValue.data( | ||||||
|  |         _sortMessages([localMessage, ...currentMessages]), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async { | ||||||
|  |     if (remoteMessage.chatRoomId != _roomId) return; | ||||||
|  |     developer.log( | ||||||
|  |       'Received message update ${remoteMessage.id}', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final updatedMessage = LocalChatMessage.fromRemoteMessage( | ||||||
|  |       remoteMessage, | ||||||
|  |       MessageStatus.sent, | ||||||
|  |     ); | ||||||
|  |     await _database.updateMessage(_database.messageToCompanion(updatedMessage)); | ||||||
|  |  | ||||||
|  |     final currentMessages = state.value ?? []; | ||||||
|  |     final index = currentMessages.indexWhere((m) => m.id == updatedMessage.id); | ||||||
|  |  | ||||||
|  |     if (index >= 0) { | ||||||
|  |       final newList = [...currentMessages]; | ||||||
|  |       newList[index] = updatedMessage; | ||||||
|  |       state = AsyncValue.data(_sortMessages(newList)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> receiveMessageDeletion(String messageId) async { | ||||||
|  |     developer.log( | ||||||
|  |       'Received message deletion $messageId', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |     _pendingMessages.remove(messageId); | ||||||
|  |  | ||||||
|  |     final currentMessages = state.value ?? []; | ||||||
|  |     final messageIndex = currentMessages.indexWhere((m) => m.id == messageId); | ||||||
|  |  | ||||||
|  |     LocalChatMessage? messageToUpdate; | ||||||
|  |     if (messageIndex != -1) { | ||||||
|  |       messageToUpdate = currentMessages[messageIndex]; | ||||||
|  |     } else { | ||||||
|  |       messageToUpdate = await fetchMessageById(messageId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (messageToUpdate == null) return; | ||||||
|  |  | ||||||
|  |     final remote = messageToUpdate.toRemoteMessage(); | ||||||
|  |     final updatedRemote = remote.copyWith( | ||||||
|  |       content: 'This message was deleted', | ||||||
|  |       deletedAt: DateTime.now(), | ||||||
|  |       attachments: [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final deletedMessage = LocalChatMessage.fromRemoteMessage( | ||||||
|  |       updatedRemote, | ||||||
|  |       messageToUpdate.status, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     await _database.saveMessage(_database.messageToCompanion(deletedMessage)); | ||||||
|  |  | ||||||
|  |     if (messageIndex != -1) { | ||||||
|  |       final newList = [...currentMessages]; | ||||||
|  |       newList[messageIndex] = deletedMessage; | ||||||
|  |       state = AsyncValue.data(newList); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> deleteMessage(String messageId) async { | ||||||
|  |     developer.log('Deleting message $messageId', name: 'MessagesNotifier'); | ||||||
|  |     try { | ||||||
|  |       await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId'); | ||||||
|  |       await receiveMessageDeletion(messageId); | ||||||
|  |     } catch (err, stackTrace) { | ||||||
|  |       developer.log( | ||||||
|  |         'Error deleting message $messageId', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |         error: err, | ||||||
|  |         stackTrace: stackTrace, | ||||||
|  |       ); | ||||||
|  |       showErrorAlert(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void searchMessages(String query, {bool? withLinks, bool? withAttachments}) { | ||||||
|  |     _searchQuery = query.trim(); | ||||||
|  |     _withLinks = withLinks; | ||||||
|  |     _withAttachments = withAttachments; | ||||||
|  |     loadInitial(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void clearSearch() { | ||||||
|  |     _searchQuery = null; | ||||||
|  |     _withLinks = null; | ||||||
|  |     _withAttachments = null; | ||||||
|  |     loadInitial(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<LocalChatMessage?> fetchMessageById(String messageId) async { | ||||||
|  |     developer.log( | ||||||
|  |       'Fetching message by id $messageId', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |     try { | ||||||
|  |       final localMessage = | ||||||
|  |           await (_database.select(_database.chatMessages) | ||||||
|  |             ..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull(); | ||||||
|  |       if (localMessage != null) { | ||||||
|  |         return _database.companionToMessage(localMessage); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       final response = await _apiClient.get( | ||||||
|  |         '/sphere/chat/$_roomId/messages/$messageId', | ||||||
|  |       ); | ||||||
|  |       final remoteMessage = SnChatMessage.fromJson(response.data); | ||||||
|  |       final message = LocalChatMessage.fromRemoteMessage( | ||||||
|  |         remoteMessage, | ||||||
|  |         MessageStatus.sent, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       await _database.saveMessage(_database.messageToCompanion(message)); | ||||||
|  |       return message; | ||||||
|  |     } catch (e) { | ||||||
|  |       if (e is DioException) return null; | ||||||
|  |       rethrow; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<int> jumpToMessage(String messageId) async { | ||||||
|  |     developer.log( | ||||||
|  |       'Starting jump to message $messageId', | ||||||
|  |       name: 'MessagesNotifier', | ||||||
|  |     ); | ||||||
|  |     if (_isJumping) { | ||||||
|  |       developer.log( | ||||||
|  |         'Jump already in progress, skipping', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |       return -1; | ||||||
|  |     } | ||||||
|  |     _isJumping = true; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       developer.log('Fetching message $messageId', name: 'MessagesNotifier'); | ||||||
|  |       final message = await fetchMessageById(messageId); | ||||||
|  |       if (message == null) { | ||||||
|  |         developer.log('Message $messageId not found', name: 'MessagesNotifier'); | ||||||
|  |         showSnackBar('messageNotFound'.tr()); | ||||||
|  |         return -1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Check if message is already in current state to avoid duplicate loading | ||||||
|  |       final currentMessages = state.value ?? []; | ||||||
|  |       final existingIndex = currentMessages.indexWhere( | ||||||
|  |         (m) => m.id == messageId, | ||||||
|  |       ); | ||||||
|  |       if (existingIndex >= 0) { | ||||||
|  |         developer.log( | ||||||
|  |           'Message $messageId already in current state at index $existingIndex, jumping directly', | ||||||
|  |           name: 'MessagesNotifier', | ||||||
|  |         ); | ||||||
|  |         return existingIndex; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       developer.log( | ||||||
|  |         'Message $messageId not in current state, loading messages around it', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // Count messages newer than this one | ||||||
|  |       final query = _database.customSelect( | ||||||
|  |         'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?', | ||||||
|  |         variables: [ | ||||||
|  |           Variable.withString(_roomId), | ||||||
|  |           Variable.withDateTime(message.createdAt), | ||||||
|  |         ], | ||||||
|  |         readsFrom: {_database.chatMessages}, | ||||||
|  |       ); | ||||||
|  |       final result = await query.getSingle(); | ||||||
|  |       final newerCount = result.read<int>('count'); | ||||||
|  |  | ||||||
|  |       // Load messages around this position | ||||||
|  |       final offset = | ||||||
|  |           (newerCount - _pageSize ~/ 2).clamp(0, double.infinity).toInt(); | ||||||
|  |       developer.log( | ||||||
|  |         'Loading messages with offset $offset, take $_pageSize', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |       final loadedMessages = await _getCachedMessages( | ||||||
|  |         offset: offset, | ||||||
|  |         take: _pageSize, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // Check if loaded messages are already in current state | ||||||
|  |       final currentIds = currentMessages.map((m) => m.id).toSet(); | ||||||
|  |       final newMessages = | ||||||
|  |           loadedMessages.where((m) => !currentIds.contains(m.id)).toList(); | ||||||
|  |       developer.log( | ||||||
|  |         'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (newMessages.isNotEmpty) { | ||||||
|  |         // Merge with current messages | ||||||
|  |         final allMessages = [...currentMessages, ...newMessages]; | ||||||
|  |         final uniqueMessages = <LocalChatMessage>[]; | ||||||
|  |         final seenIds = <String>{}; | ||||||
|  |         for (final message in allMessages) { | ||||||
|  |           if (seenIds.add(message.id)) { | ||||||
|  |             uniqueMessages.add(message); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         _sortMessages(uniqueMessages); | ||||||
|  |         state = AsyncValue.data(uniqueMessages); | ||||||
|  |         developer.log( | ||||||
|  |           'Updated state with ${uniqueMessages.length} total messages', | ||||||
|  |           name: 'MessagesNotifier', | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       final finalIndex = (state.value ?? []).indexWhere( | ||||||
|  |         (m) => m.id == messageId, | ||||||
|  |       ); | ||||||
|  |       developer.log( | ||||||
|  |         'Final index for message $messageId is $finalIndex', | ||||||
|  |         name: 'MessagesNotifier', | ||||||
|  |       ); | ||||||
|  |       return finalIndex; | ||||||
|  |     } finally { | ||||||
|  |       _isJumping = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bool _hasLink(LocalChatMessage message) { | ||||||
|  |     final content = message.toRemoteMessage().content; | ||||||
|  |     if (content == null) return false; | ||||||
|  |     final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*'); | ||||||
|  |     return urlRegex.hasMatch(content); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,12 +1,12 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'room.dart'; | part of 'messages_notifier.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| 
 | 
 | ||||||
| String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4'; | String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237'; | ||||||
| 
 | 
 | ||||||
| /// Copied from Dart SDK | /// Copied from Dart SDK | ||||||
| class _SystemHash { | class _SystemHash { | ||||||
| @@ -30,7 +30,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | |||||||
|       final user = SnAccount.fromJson(response.data); |       final user = SnAccount.fromJson(response.data); | ||||||
|       state = AsyncValue.data(user); |       state = AsyncValue.data(user); | ||||||
|  |  | ||||||
|       if (kIsWeb || !Platform.isLinux) { |       if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) { | ||||||
|         FirebaseAnalytics.instance.setUserId(id: user.id); |         FirebaseAnalytics.instance.setUserId(id: user.id); | ||||||
|       } |       } | ||||||
|     } catch (error, stackTrace) { |     } catch (error, stackTrace) { | ||||||
| @@ -44,9 +44,12 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | |||||||
|                       : 'failedToLoadUserInfoNetwork') |                       : 'failedToLoadUserInfoNetwork') | ||||||
|                   .tr() |                   .tr() | ||||||
|                   .trim(), |                   .trim(), | ||||||
|               '${error.response!.statusCode}\n${error.response?.headers}', |               '', | ||||||
|  |               '${error.response?.statusCode ?? 'Network Error'}', | ||||||
|  |               if (error.response?.headers != null) error.response?.headers, | ||||||
|  |               if (error.response?.data != null) | ||||||
|                 jsonEncode(error.response?.data), |                 jsonEncode(error.response?.data), | ||||||
|             ].join('\n\n'), |             ].join('\n'), | ||||||
|             iconStyle: IconStyle.error, |             iconStyle: IconStyle.error, | ||||||
|             neutralButtonTitle: 'retry'.tr(), |             neutralButtonTitle: 'retry'.tr(), | ||||||
|             negativeButtonTitle: 'okay'.tr(), |             negativeButtonTitle: 'okay'.tr(), | ||||||
| @@ -87,7 +90,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); | ||||||
|     if (kIsWeb || !Platform.isLinux) { |     if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) { | ||||||
|       FirebaseAnalytics.instance.setUserId(id: null); |       FirebaseAnalytics.instance.setUserId(id: null); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart' show kIsWeb; | |||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/screens/about.dart'; | import 'package:island/screens/about.dart'; | ||||||
| import 'package:island/screens/account/credits.dart'; |  | ||||||
| import 'package:island/screens/developers/app_detail.dart'; | import 'package:island/screens/developers/app_detail.dart'; | ||||||
| import 'package:island/screens/developers/bot_detail.dart'; | import 'package:island/screens/developers/bot_detail.dart'; | ||||||
| import 'package:island/screens/developers/edit_app.dart'; | import 'package:island/screens/developers/edit_app.dart'; | ||||||
| @@ -19,6 +18,7 @@ import 'package:island/screens/developers/edit_project.dart'; | |||||||
| import 'package:island/screens/developers/new_project.dart'; | import 'package:island/screens/developers/new_project.dart'; | ||||||
| import 'package:island/screens/developers/project_detail.dart'; | import 'package:island/screens/developers/project_detail.dart'; | ||||||
| import 'package:island/screens/discovery/articles.dart'; | import 'package:island/screens/discovery/articles.dart'; | ||||||
|  | import 'package:island/screens/files/file_list.dart'; | ||||||
| import 'package:island/screens/posts/post_categories_list.dart'; | import 'package:island/screens/posts/post_categories_list.dart'; | ||||||
| import 'package:island/screens/posts/post_category_detail.dart'; | import 'package:island/screens/posts/post_category_detail.dart'; | ||||||
| import 'package:island/screens/posts/post_search.dart'; | import 'package:island/screens/posts/post_search.dart'; | ||||||
| @@ -38,7 +38,7 @@ import 'package:island/screens/chat/chat.dart'; | |||||||
| import 'package:island/screens/chat/room.dart'; | import 'package:island/screens/chat/room.dart'; | ||||||
| import 'package:island/screens/chat/room_detail.dart'; | import 'package:island/screens/chat/room_detail.dart'; | ||||||
| import 'package:island/screens/chat/call.dart'; | import 'package:island/screens/chat/call.dart'; | ||||||
| import 'package:island/screens/chat/search_messages_screen.dart'; | import 'package:island/screens/chat/search_messages.dart'; | ||||||
| import 'package:island/screens/creators/hub.dart'; | import 'package:island/screens/creators/hub.dart'; | ||||||
| import 'package:island/screens/creators/posts/post_manage_list.dart'; | import 'package:island/screens/creators/posts/post_manage_list.dart'; | ||||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | import 'package:island/screens/creators/stickers/stickers.dart'; | ||||||
| @@ -86,11 +86,7 @@ Widget _tabPagesTransitionBuilder( | |||||||
| } | } | ||||||
|  |  | ||||||
| bool get _supportsAnalytics => | bool get _supportsAnalytics => | ||||||
|     kIsWeb || |     kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS; | ||||||
|     Platform.isAndroid || |  | ||||||
|     Platform.isIOS || |  | ||||||
|     Platform.isMacOS || |  | ||||||
|     Platform.isWindows; |  | ||||||
|  |  | ||||||
| // Provider for the router | // Provider for the router | ||||||
| final routerProvider = Provider<GoRouter>((ref) { | final routerProvider = Provider<GoRouter>((ref) { | ||||||
| @@ -660,9 +656,9 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|                     builder: (context, state) => const WalletScreen(), |                     builder: (context, state) => const WalletScreen(), | ||||||
|                   ), |                   ), | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     name: 'socialCredits', |                     name: 'files', | ||||||
|                     path: '/account/credits', |                     path: '/account/files', | ||||||
|                     builder: (context, state) => const SocialCreditsScreen(), |                     builder: (context, state) => const FileListScreen(), | ||||||
|                   ), |                   ), | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     name: 'relationships', |                     name: 'relationships', | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:flutter/services.dart'; | 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.dart' as udid; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| @@ -68,7 +68,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | |||||||
|     try { |     try { | ||||||
|       final deviceInfoPlugin = DeviceInfoPlugin(); |       final deviceInfoPlugin = DeviceInfoPlugin(); | ||||||
|       _deviceInfo = await deviceInfoPlugin.deviceInfo; |       _deviceInfo = await deviceInfoPlugin.deviceInfo; | ||||||
|       _deviceUdid = await getUdid(); |       _deviceUdid = await udid.getUdid(); | ||||||
|       if (mounted) { |       if (mounted) { | ||||||
|         setState(() {}); |         setState(() {}); | ||||||
|       } |       } | ||||||
| @@ -174,12 +174,20 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | |||||||
|                             context, |                             context, | ||||||
|                             title: 'Device Information', |                             title: 'Device Information', | ||||||
|                             children: [ |                             children: [ | ||||||
|                               _buildInfoItem( |                               FutureBuilder<String>( | ||||||
|  |                                 future: udid.getDeviceName(), | ||||||
|  |                                 builder: (context, snapshot) { | ||||||
|  |                                   final value = | ||||||
|  |                                       snapshot.hasData | ||||||
|  |                                           ? snapshot.data! | ||||||
|  |                                           : 'unknown'.tr(); | ||||||
|  |                                   return _buildInfoItem( | ||||||
|                                     context, |                                     context, | ||||||
|                                     icon: Symbols.label, |                                     icon: Symbols.label, | ||||||
|                                     label: 'aboutDeviceName'.tr(), |                                     label: 'aboutDeviceName'.tr(), | ||||||
|                                 value: |                                     value: value, | ||||||
|                                     _deviceInfo?.data['name'] ?? 'unknown'.tr(), |                                   ); | ||||||
|  |                                 }, | ||||||
|                               ), |                               ), | ||||||
|                               _buildInfoItem( |                               _buildInfoItem( | ||||||
|                                 context, |                                 context, | ||||||
|   | |||||||
| @@ -141,20 +141,22 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
|             ).padding(horizontal: 8), |             ).padding(horizontal: 8), | ||||||
|             GestureDetector( |             LevelingProgressCard( | ||||||
|               child: LevelingProgressCard( |               isCompact: true, | ||||||
|               level: user.value!.profile.level, |               level: user.value!.profile.level, | ||||||
|               experience: user.value!.profile.experience, |               experience: user.value!.profile.experience, | ||||||
|               progress: user.value!.profile.levelingProgress, |               progress: user.value!.profile.levelingProgress, | ||||||
|               ), |  | ||||||
|               onTap: () { |               onTap: () { | ||||||
|                 context.pushNamed('leveling'); |                 context.pushNamed('leveling'); | ||||||
|               }, |               }, | ||||||
|             ).padding(horizontal: 12), |             ).padding(horizontal: 12), | ||||||
|  |             const SizedBox.shrink(), | ||||||
|             Row( |             Row( | ||||||
|  |               spacing: 8, | ||||||
|               children: [ |               children: [ | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: Card( |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|                     child: InkWell( |                     child: InkWell( | ||||||
|                       borderRadius: BorderRadius.circular(8), |                       borderRadius: BorderRadius.circular(8), | ||||||
|                       child: Column( |                       child: Column( | ||||||
| @@ -181,6 +183,7 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 ), |                 ), | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: Card( |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|                     child: InkWell( |                     child: InkWell( | ||||||
|                       borderRadius: BorderRadius.circular(8), |                       borderRadius: BorderRadius.circular(8), | ||||||
|                       child: Column( |                       child: Column( | ||||||
| @@ -206,7 +209,73 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                   ).height(140), |                   ).height(140), | ||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ).padding(horizontal: 8), |             ).padding(horizontal: 12), | ||||||
|  |             const SizedBox.shrink(), | ||||||
|  |             Row( | ||||||
|  |               spacing: 8, | ||||||
|  |               children: [ | ||||||
|  |                 Expanded( | ||||||
|  |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|  |                     child: InkWell( | ||||||
|  |                       borderRadius: BorderRadius.circular(8), | ||||||
|  |                       child: Column( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon(Symbols.settings, size: 28).padding(bottom: 8), | ||||||
|  |                           Text('appSettings').tr().fontSize(16).bold(), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 16, vertical: 12), | ||||||
|  |                       onTap: () { | ||||||
|  |                         context.pushNamed('settings'); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ).height(120), | ||||||
|  |                 ), | ||||||
|  |                 Expanded( | ||||||
|  |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|  |                     child: InkWell( | ||||||
|  |                       borderRadius: BorderRadius.circular(8), | ||||||
|  |                       child: Column( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon( | ||||||
|  |                             Symbols.person_edit, | ||||||
|  |                             size: 28, | ||||||
|  |                           ).padding(bottom: 8), | ||||||
|  |                           Text('updateYourProfile').tr().fontSize(16).bold(), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 16, vertical: 12), | ||||||
|  |                       onTap: () { | ||||||
|  |                         context.pushNamed('profileUpdate'); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ).height(120), | ||||||
|  |                 ), | ||||||
|  |                 Expanded( | ||||||
|  |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|  |                     child: InkWell( | ||||||
|  |                       borderRadius: BorderRadius.circular(8), | ||||||
|  |                       child: Column( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon( | ||||||
|  |                             Symbols.manage_accounts, | ||||||
|  |                             size: 28, | ||||||
|  |                           ).padding(bottom: 8), | ||||||
|  |                           Text('accountSettings').tr().fontSize(16).bold(), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 16, vertical: 12), | ||||||
|  |                       onTap: () { | ||||||
|  |                         context.pushNamed('accountSettings'); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ).height(120), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ).padding(horizontal: 12), | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.notifications), |               leading: const Icon(Symbols.notifications), | ||||||
| @@ -235,6 +304,16 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 context.pushNamed('wallet'); |                 context.pushNamed('wallet'); | ||||||
|               }, |               }, | ||||||
|             ), |             ), | ||||||
|  |             ListTile( | ||||||
|  |               minTileHeight: 48, | ||||||
|  |               leading: const Icon(Symbols.files), | ||||||
|  |               trailing: const Icon(Symbols.chevron_right), | ||||||
|  |               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |               title: Text('files').tr(), | ||||||
|  |               onTap: () { | ||||||
|  |                 context.pushNamed('files'); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.people), |               leading: const Icon(Symbols.people), | ||||||
| @@ -265,16 +344,6 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 context.pushNamed('webFeedMarketplace'); |                 context.pushNamed('webFeedMarketplace'); | ||||||
|               }, |               }, | ||||||
|             ), |             ), | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.star), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('credits').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('socialCredits'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               title: Text('abuseReport').tr(), |               title: Text('abuseReport').tr(), | ||||||
| @@ -284,37 +353,6 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|               onTap: () => context.pushNamed('reportList'), |               onTap: () => context.pushNamed('reportList'), | ||||||
|             ), |             ), | ||||||
|             const Divider(height: 1).padding(vertical: 8), |             const Divider(height: 1).padding(vertical: 8), | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.settings), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('appSettings').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('settings'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.person_edit), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('updateYourProfile').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('profileUpdate'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.manage_accounts), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('accountSettings').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('accountSettings'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             const Divider(height: 1).padding(vertical: 8), |  | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.info), |               leading: const Icon(Symbols.info), | ||||||
| @@ -333,6 +371,8 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|               title: Text('debugOptions').tr(), |               title: Text('debugOptions').tr(), | ||||||
|               onTap: () { |               onTap: () { | ||||||
|                 showModalBottomSheet( |                 showModalBottomSheet( | ||||||
|  |                   useRootNavigator: true, | ||||||
|  |                   isScrollControlled: true, | ||||||
|                   context: context, |                   context: context, | ||||||
|                   builder: (context) => DebugSheet(), |                   builder: (context) => DebugSheet(), | ||||||
|                 ); |                 ); | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/account.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; |  | ||||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | import 'package:material_symbols_icons/material_symbols_icons.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'; | ||||||
| @@ -59,19 +58,17 @@ class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class SocialCreditsScreen extends HookConsumerWidget { | class SocialCreditsTab extends HookConsumerWidget { | ||||||
|   const SocialCreditsScreen({super.key}); |   const SocialCreditsTab({super.key}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final socialCredits = ref.watch(socialCreditsProvider); |     final socialCredits = ref.watch(socialCreditsProvider); | ||||||
|  |     return Column( | ||||||
|     return AppScaffold( |  | ||||||
|       appBar: AppBar(title: Text('socialCredits').tr()), |  | ||||||
|       body: Column( |  | ||||||
|       children: [ |       children: [ | ||||||
|  |         const Gap(8), | ||||||
|         Card( |         Card( | ||||||
|             margin: EdgeInsets.only(left: 16, right: 16, top: 8), |           margin: const EdgeInsets.only(left: 16, right: 16, top: 8), | ||||||
|           child: socialCredits |           child: socialCredits | ||||||
|               .when( |               .when( | ||||||
|                 data: |                 data: | ||||||
| @@ -127,7 +124,9 @@ class SocialCreditsScreen extends HookConsumerWidget { | |||||||
|                     } |                     } | ||||||
|                     final record = data.items[index]; |                     final record = data.items[index]; | ||||||
|                     return ListTile( |                     return ListTile( | ||||||
|                         contentPadding: EdgeInsets.symmetric(horizontal: 24), |                       contentPadding: const EdgeInsets.symmetric( | ||||||
|  |                         horizontal: 24, | ||||||
|  |                       ), | ||||||
|                       title: Text(record.reason), |                       title: Text(record.reason), | ||||||
|                       subtitle: Text( |                       subtitle: Text( | ||||||
|                         DateFormat.yMMMd().format(record.createdAt), |                         DateFormat.yMMMd().format(record.createdAt), | ||||||
| @@ -146,7 +145,6 @@ class SocialCreditsScreen extends HookConsumerWidget { | |||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|       ], |       ], | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,12 +4,12 @@ import 'package:dio/dio.dart'; | |||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.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/account.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/models/wallet.dart'; | import 'package:island/models/wallet.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
|  | import 'package:island/screens/account/credits.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
| import 'package:island/widgets/account/leveling_progress.dart'; | import 'package:island/widgets/account/leveling_progress.dart'; | ||||||
| @@ -89,7 +89,7 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return DefaultTabController( |     return DefaultTabController( | ||||||
|       length: 2, |       length: 3, | ||||||
|       child: AppScaffold( |       child: AppScaffold( | ||||||
|         appBar: AppBar( |         appBar: AppBar( | ||||||
|           title: Text('levelingProgress'.tr()), |           title: Text('levelingProgress'.tr()), | ||||||
| @@ -104,6 +104,15 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|  |               Tab( | ||||||
|  |                 child: Text( | ||||||
|  |                   'socialCredits'.tr(), | ||||||
|  |                   textAlign: TextAlign.center, | ||||||
|  |                   style: TextStyle( | ||||||
|  |                     color: Theme.of(context).appBarTheme.foregroundColor!, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|               Tab( |               Tab( | ||||||
|                 child: Text( |                 child: Text( | ||||||
|                   'stellarProgram'.tr(), |                   'stellarProgram'.tr(), | ||||||
| @@ -119,6 +128,7 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|         body: TabBarView( |         body: TabBarView( | ||||||
|           children: [ |           children: [ | ||||||
|             _buildLevelingTab(context, ref, user.value!), |             _buildLevelingTab(context, ref, user.value!), | ||||||
|  |             const SocialCreditsTab(), | ||||||
|             _buildStellarProgramTab(context, ref), |             _buildStellarProgramTab(context, ref), | ||||||
|           ], |           ], | ||||||
|         ), |         ), | ||||||
| @@ -138,7 +148,6 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|     return Center( |     return Center( | ||||||
|       child: Container( |       child: Container( | ||||||
|         padding: const EdgeInsets.symmetric(horizontal: 20), |         padding: const EdgeInsets.symmetric(horizontal: 20), | ||||||
|         constraints: const BoxConstraints(maxWidth: 480), |  | ||||||
|         child: CustomScrollView( |         child: CustomScrollView( | ||||||
|           slivers: [ |           slivers: [ | ||||||
|             const SliverGap(20), |             const SliverGap(20), | ||||||
| @@ -164,10 +173,33 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|             ), |             ), | ||||||
|             const SliverGap(16), |             const SliverGap(16), | ||||||
|  |  | ||||||
|             // Stairs visualization with fixed height and horizontal scroll |             SliverToBoxAdapter( | ||||||
|             SliverToBoxAdapter(child: _buildLevelStairs(context, currentLevel)), |               child: Card( | ||||||
|             const SliverGap(24), |                 margin: EdgeInsets.zero, | ||||||
|  |                 child: Column( | ||||||
|  |                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |                   children: [ | ||||||
|  |                     Text( | ||||||
|  |                       '${'levelingProgressLevel'.tr(args: [currentLevel.toString()])} / 120', | ||||||
|  |                       textAlign: TextAlign.start, | ||||||
|  |                       style: Theme.of(context).textTheme.bodySmall, | ||||||
|  |                     ), | ||||||
|  |                     const Gap(8), | ||||||
|  |                     LinearProgressIndicator( | ||||||
|  |                       value: currentLevel / 120, | ||||||
|  |                       minHeight: 10, | ||||||
|  |                       stopIndicatorRadius: 0, | ||||||
|  |                       trackGap: 0, | ||||||
|  |                       color: Theme.of(context).colorScheme.primary, | ||||||
|  |                       backgroundColor: | ||||||
|  |                           Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |                       borderRadius: BorderRadius.circular(32), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ).padding(horizontal: 16, top: 16, bottom: 12), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             const SliverGap(16), | ||||||
|             // Leveling History |             // Leveling History | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
|               child: Text( |               child: Text( | ||||||
| @@ -239,9 +271,6 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     return SingleChildScrollView( |     return SingleChildScrollView( | ||||||
|       padding: getTabbedPadding(context, horizontal: 20, vertical: 20), |       padding: getTabbedPadding(context, horizontal: 20, vertical: 20), | ||||||
|       child: Center( |  | ||||||
|         child: ConstrainedBox( |  | ||||||
|           constraints: const BoxConstraints(maxWidth: 480), |  | ||||||
|       child: Column( |       child: Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|         children: [ |         children: [ | ||||||
| @@ -249,128 +278,6 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|           const Gap(16), |           const Gap(16), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildLevelStairs(BuildContext context, int currentLevel) { |  | ||||||
|     const totalLevels = 14; |  | ||||||
|     const stairHeight = 20.0; |  | ||||||
|     const stairWidth = 50.0; |  | ||||||
|     const containerHeight = 280.0; |  | ||||||
|  |  | ||||||
|     return Container( |  | ||||||
|       height: containerHeight, |  | ||||||
|       decoration: BoxDecoration( |  | ||||||
|         borderRadius: BorderRadius.circular(12), |  | ||||||
|         border: Border.all( |  | ||||||
|           color: Theme.of(context).colorScheme.outline.withOpacity(0.2), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       child: SingleChildScrollView( |  | ||||||
|         scrollDirection: Axis.horizontal, |  | ||||||
|         padding: const EdgeInsets.symmetric(horizontal: 16), |  | ||||||
|         child: SizedBox( |  | ||||||
|           width: (totalLevels * (stairWidth + 8)) + 40, |  | ||||||
|           height: containerHeight, |  | ||||||
|           child: CustomPaint( |  | ||||||
|             painter: LevelStairsPainter( |  | ||||||
|               currentLevel: currentLevel, |  | ||||||
|               totalLevels: totalLevels, |  | ||||||
|               primaryColor: Theme.of(context).colorScheme.primary, |  | ||||||
|               surfaceColor: Theme.of(context).colorScheme.surfaceContainerHigh, |  | ||||||
|               onSurfaceColor: Theme.of(context).colorScheme.onSurface, |  | ||||||
|               stairHeight: stairHeight, |  | ||||||
|               stairWidth: stairWidth, |  | ||||||
|             ), |  | ||||||
|             child: Stack( |  | ||||||
|               children: List.generate(totalLevels, (index) { |  | ||||||
|                 final level = index + 1; |  | ||||||
|                 final isCompleted = level <= currentLevel; |  | ||||||
|                 final isCurrent = level == currentLevel; |  | ||||||
|  |  | ||||||
|                 // Calculate position from bottom |  | ||||||
|                 final bottomPosition = 0.0; |  | ||||||
|                 final leftPosition = 20.0 + (index * (stairWidth + 8)); |  | ||||||
|  |  | ||||||
|                 // Make higher levels progressively taller |  | ||||||
|                 final progressiveHeight = |  | ||||||
|                     40.0 + (index * 15.0); // Base height + progressive increase |  | ||||||
|  |  | ||||||
|                 return Positioned( |  | ||||||
|                   left: leftPosition, |  | ||||||
|                   bottom: bottomPosition, |  | ||||||
|                   child: Container( |  | ||||||
|                     width: stairWidth, |  | ||||||
|                     height: progressiveHeight, |  | ||||||
|                     decoration: BoxDecoration( |  | ||||||
|                       color: |  | ||||||
|                           isCompleted |  | ||||||
|                               ? Theme.of(context).colorScheme.primary |  | ||||||
|                               : Theme.of( |  | ||||||
|                                 context, |  | ||||||
|                               ).colorScheme.surfaceContainerHigh, |  | ||||||
|                       borderRadius: const BorderRadius.only( |  | ||||||
|                         topLeft: Radius.circular(6), |  | ||||||
|                         topRight: Radius.circular(6), |  | ||||||
|                       ), |  | ||||||
|                       border: |  | ||||||
|                           isCurrent |  | ||||||
|                               ? Border.all( |  | ||||||
|                                 color: Theme.of(context).colorScheme.primary, |  | ||||||
|                                 width: 2, |  | ||||||
|                               ) |  | ||||||
|                               : null, |  | ||||||
|                       boxShadow: |  | ||||||
|                           isCurrent |  | ||||||
|                               ? [ |  | ||||||
|                                 BoxShadow( |  | ||||||
|                                   color: Theme.of( |  | ||||||
|                                     context, |  | ||||||
|                                   ).colorScheme.primary.withOpacity(0.3), |  | ||||||
|                                   blurRadius: 6, |  | ||||||
|                                   spreadRadius: 1, |  | ||||||
|                                 ), |  | ||||||
|                               ] |  | ||||||
|                               : null, |  | ||||||
|                     ), |  | ||||||
|                     child: Padding( |  | ||||||
|                       padding: const EdgeInsets.only(top: 8), |  | ||||||
|                       child: Column( |  | ||||||
|                         children: [ |  | ||||||
|                           Text( |  | ||||||
|                             level.toString(), |  | ||||||
|                             style: GoogleFonts.robotoMono( |  | ||||||
|                               fontSize: 14, |  | ||||||
|                               fontWeight: FontWeight.bold, |  | ||||||
|                               color: |  | ||||||
|                                   isCompleted |  | ||||||
|                                       ? Theme.of(context).colorScheme.onPrimary |  | ||||||
|                                       : Theme.of(context).colorScheme.onSurface, |  | ||||||
|                             ), |  | ||||||
|                           ), |  | ||||||
|                           if (isCurrent) ...[ |  | ||||||
|                             const Gap(4), |  | ||||||
|                             Container( |  | ||||||
|                               width: 4, |  | ||||||
|                               height: 4, |  | ||||||
|                               decoration: BoxDecoration( |  | ||||||
|                                 color: Theme.of(context).colorScheme.onPrimary, |  | ||||||
|                                 shape: BoxShape.circle, |  | ||||||
|                               ), |  | ||||||
|                             ), |  | ||||||
|                           ], |  | ||||||
|                         ], |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                   ), |  | ||||||
|                 ); |  | ||||||
|               }), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|         final token = await getToken(ref.watch(tokenProvider)); |         final token = await getToken(ref.watch(tokenProvider)); | ||||||
|         if (token == null) throw ArgumentError('Token is null'); |         if (token == null) throw ArgumentError('Token is null'); | ||||||
|         final cloudFile = |         final cloudFile = | ||||||
|             await putMediaToCloud( |             await putFileToCloud( | ||||||
|               fileData: UniversalFile( |               fileData: UniversalFile( | ||||||
|                 data: result, |                 data: result, | ||||||
|                 type: UniversalFileType.image, |                 type: UniversalFileType.image, | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ import 'package:island/widgets/content/cloud_files.dart'; | |||||||
| import 'package:island/widgets/content/markdown.dart'; | import 'package:island/widgets/content/markdown.dart'; | ||||||
| import 'package:island/widgets/safety/abuse_report_helper.dart'; | import 'package:island/widgets/safety/abuse_report_helper.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:palette_generator/palette_generator.dart'; | import 'package:island/services/color_extraction.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:share_plus/share_plus.dart'; | import 'package:share_plus/share_plus.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| @@ -581,14 +581,14 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { | |||||||
|   try { |   try { | ||||||
|     final account = await ref.watch(accountProvider(uname).future); |     final account = await ref.watch(accountProvider(uname).future); | ||||||
|     if (account.profile.background == null) return null; |     if (account.profile.background == null) return null; | ||||||
|     final palette = await PaletteGenerator.fromImageProvider( |     final colors = await ColorExtractionService.getColorsFromImage( | ||||||
|       CloudImageWidget.provider( |       CloudImageWidget.provider( | ||||||
|         fileId: account.profile.background!.id, |         fileId: account.profile.background!.id, | ||||||
|         serverUrl: ref.watch(serverUrlProvider), |         serverUrl: ref.watch(serverUrlProvider), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|     final dominantColor = palette.dominantColor?.color; |     if (colors.isEmpty) return null; | ||||||
|     if (dominantColor == null) return null; |     final dominantColor = colors.first; | ||||||
|     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; |     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||||
|   } catch (_) { |   } catch (_) { | ||||||
|     return null; |     return null; | ||||||
|   | |||||||
| @@ -268,7 +268,7 @@ class _AccountBadgesProviderElement | |||||||
| } | } | ||||||
|  |  | ||||||
| String _$accountAppbarForcegroundColorHash() => | String _$accountAppbarForcegroundColorHash() => | ||||||
|     r'8ee0cae10817b77fb09548a482f5247662b4374c'; |     r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0'; | ||||||
|  |  | ||||||
| /// See also [accountAppbarForcegroundColor]. | /// See also [accountAppbarForcegroundColor]. | ||||||
| @ProviderFor(accountAppbarForcegroundColor) | @ProviderFor(accountAppbarForcegroundColor) | ||||||
|   | |||||||
| @@ -1,17 +1,10 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/pods/network.dart'; |  | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|  |  | ||||||
| part 'captcha.config.g.dart'; | part 'captcha.config.g.dart'; | ||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| Future<String> captchaUrl(Ref ref) async { | Future<String> captchaUrl(Ref ref) async { | ||||||
|   final apiClient = ref.watch(apiClientProvider); |   const baseUrl = "https://solian.app"; | ||||||
|   final resp = await apiClient.get('/.well-known/services'); |   return '$baseUrl/auth/captcha'; | ||||||
|   final serviceMapping = await resp.data; |  | ||||||
|   var baseUrl = serviceMapping['DysonNetwork.Pass'] as String; |  | ||||||
|   // The backend using self-signed certicates on development |  | ||||||
|   // Which mobile simulator might not accept, use this to avoid errors |  | ||||||
|   if (baseUrl.contains('https://localhost')) baseUrl = 'http://localhost:5216'; |  | ||||||
|   return '$baseUrl/captcha'; |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ part of 'captcha.config.dart'; | |||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$captchaUrlHash() => r'bbed0d18272dd205069642b3c6583ea2eef735d1'; | String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8'; | ||||||
|  |  | ||||||
| /// See also [captchaUrl]. | /// See also [captchaUrl]. | ||||||
| @ProviderFor(captchaUrl) | @ProviderFor(captchaUrl) | ||||||
|   | |||||||
| @@ -1,9 +1,6 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:io'; |  | ||||||
| import 'dart:math' as math; | import 'dart:math' as math; | ||||||
|  |  | ||||||
| import 'package:animations/animations.dart'; | import 'package:animations/animations.dart'; | ||||||
| import 'package:device_info_plus/device_info_plus.dart'; |  | ||||||
| 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/foundation.dart'; | ||||||
| @@ -42,22 +39,6 @@ final Map<int, (String, String, IconData)> kFactorTypes = { | |||||||
|   4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), |   4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), | ||||||
| }; | }; | ||||||
|  |  | ||||||
| Future<String?> getDeviceName() async { |  | ||||||
|   if (kIsWeb) return null; |  | ||||||
|   String? name; |  | ||||||
|   if (Platform.isIOS) { |  | ||||||
|     final deviceInfo = await DeviceInfoPlugin().iosInfo; |  | ||||||
|     name = deviceInfo.name; |  | ||||||
|   } else if (Platform.isAndroid) { |  | ||||||
|     final deviceInfo = await DeviceInfoPlugin().androidInfo; |  | ||||||
|     name = deviceInfo.name; |  | ||||||
|   } else if (Platform.isWindows) { |  | ||||||
|     final deviceInfo = await DeviceInfoPlugin().windowsInfo; |  | ||||||
|     name = deviceInfo.computerName; |  | ||||||
|   } |  | ||||||
|   return name; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class LoginScreen extends HookConsumerWidget { | class LoginScreen extends HookConsumerWidget { | ||||||
|   const LoginScreen({super.key}); |   const LoginScreen({super.key}); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -543,7 +543,7 @@ class EditChatScreen extends HookConsumerWidget { | |||||||
|         final token = await getToken(ref.watch(tokenProvider)); |         final token = await getToken(ref.watch(tokenProvider)); | ||||||
|         if (token == null) throw ArgumentError('Token is null'); |         if (token == null) throw ArgumentError('Token is null'); | ||||||
|         final cloudFile = |         final cloudFile = | ||||||
|             await putMediaToCloud( |             await putFileToCloud( | ||||||
|               fileData: UniversalFile( |               fileData: UniversalFile( | ||||||
|                 data: result, |                 data: result, | ||||||
|                 type: UniversalFileType.image, |                 type: UniversalFileType.image, | ||||||
|   | |||||||
							
								
								
									
										221
									
								
								lib/screens/chat/public_room_preview.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								lib/screens/chat/public_room_preview.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | |||||||
|  | import "package:flutter/material.dart"; | ||||||
|  | import "package:flutter_hooks/flutter_hooks.dart"; | ||||||
|  | import "package:gap/gap.dart"; | ||||||
|  | import "package:hooks_riverpod/hooks_riverpod.dart"; | ||||||
|  | import "package:island/database/message.dart"; | ||||||
|  | import "package:island/screens/chat/chat.dart"; | ||||||
|  | import "package:island/widgets/content/cloud_files.dart"; | ||||||
|  | import "package:super_sliver_list/super_sliver_list.dart"; | ||||||
|  | import "package:easy_localization/easy_localization.dart"; | ||||||
|  | import "package:go_router/go_router.dart"; | ||||||
|  | import "package:material_symbols_icons/symbols.dart"; | ||||||
|  | import "package:styled_widget/styled_widget.dart"; | ||||||
|  | import "package:island/models/chat.dart"; | ||||||
|  | import "package:island/widgets/alert.dart"; | ||||||
|  | import "package:island/widgets/app_scaffold.dart"; | ||||||
|  | import "package:island/widgets/chat/message_item.dart"; | ||||||
|  | import "package:island/widgets/response.dart"; | ||||||
|  | import "package:island/pods/network.dart"; | ||||||
|  | import "package:island/services/responsive.dart"; | ||||||
|  | import "package:island/pods/messages_notifier.dart"; | ||||||
|  |  | ||||||
|  | class PublicRoomPreview extends HookConsumerWidget { | ||||||
|  |   final String id; | ||||||
|  |   final SnChatRoom room; | ||||||
|  |  | ||||||
|  |   const PublicRoomPreview({super.key, required this.id, required this.room}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final messages = ref.watch(messagesNotifierProvider(id)); | ||||||
|  |     final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier); | ||||||
|  |     final scrollController = useScrollController(); | ||||||
|  |  | ||||||
|  |     final listController = useMemoized(() => ListController(), []); | ||||||
|  |  | ||||||
|  |     var isLoading = false; | ||||||
|  |  | ||||||
|  |     // Add scroll listener for pagination | ||||||
|  |     useEffect(() { | ||||||
|  |       void onScroll() { | ||||||
|  |         if (scrollController.position.pixels >= | ||||||
|  |             scrollController.position.maxScrollExtent - 200) { | ||||||
|  |           if (isLoading) return; | ||||||
|  |           isLoading = true; | ||||||
|  |           messagesNotifier.loadMore().then((_) => isLoading = false); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       scrollController.addListener(onScroll); | ||||||
|  |       return () => scrollController.removeListener(onScroll); | ||||||
|  |     }, [scrollController]); | ||||||
|  |  | ||||||
|  |     Widget chatMessageListWidget(List<LocalChatMessage> messageList) => | ||||||
|  |         SuperListView.builder( | ||||||
|  |           listController: listController, | ||||||
|  |           padding: EdgeInsets.symmetric(vertical: 16), | ||||||
|  |           controller: scrollController, | ||||||
|  |           reverse: true, // Show newest messages at the bottom | ||||||
|  |           itemCount: messageList.length, | ||||||
|  |           findChildIndexCallback: (key) { | ||||||
|  |             final valueKey = key as ValueKey; | ||||||
|  |             final messageId = valueKey.value as String; | ||||||
|  |             return messageList.indexWhere((m) => m.id == messageId); | ||||||
|  |           }, | ||||||
|  |           extentEstimation: (_, _) => 40, | ||||||
|  |           itemBuilder: (context, index) { | ||||||
|  |             final message = messageList[index]; | ||||||
|  |             final nextMessage = | ||||||
|  |                 index < messageList.length - 1 ? messageList[index + 1] : null; | ||||||
|  |             final isLastInGroup = | ||||||
|  |                 nextMessage == null || | ||||||
|  |                 nextMessage.senderId != message.senderId || | ||||||
|  |                 nextMessage.createdAt | ||||||
|  |                         .difference(message.createdAt) | ||||||
|  |                         .inMinutes | ||||||
|  |                         .abs() > | ||||||
|  |                     3; | ||||||
|  |  | ||||||
|  |             return MessageItem( | ||||||
|  |               message: message, | ||||||
|  |               isCurrentUser: false, // User is not a member, so not current user | ||||||
|  |               onAction: null, // No actions allowed in preview mode | ||||||
|  |               onJump: (_) {}, // No jump functionality in preview | ||||||
|  |               progress: null, | ||||||
|  |               showAvatar: isLastInGroup, | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |     final compactHeader = isWideScreen(context); | ||||||
|  |  | ||||||
|  |     Widget comfortHeaderWidget() => Column( | ||||||
|  |       spacing: 4, | ||||||
|  |       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |       children: [ | ||||||
|  |         SizedBox( | ||||||
|  |           height: 26, | ||||||
|  |           width: 26, | ||||||
|  |           child: | ||||||
|  |               (room.type == 1 && room.picture?.id == null) | ||||||
|  |                   ? SplitAvatarWidget( | ||||||
|  |                     filesId: | ||||||
|  |                         room.members! | ||||||
|  |                             .map((e) => e.account.profile.picture?.id) | ||||||
|  |                             .toList(), | ||||||
|  |                   ) | ||||||
|  |                   : room.picture?.id != null | ||||||
|  |                   ? ProfilePictureWidget( | ||||||
|  |                     fileId: room.picture?.id, | ||||||
|  |                     fallbackIcon: Symbols.chat, | ||||||
|  |                   ) | ||||||
|  |                   : CircleAvatar( | ||||||
|  |                     child: Text( | ||||||
|  |                       room.name![0].toUpperCase(), | ||||||
|  |                       style: const TextStyle(fontSize: 12), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |         ), | ||||||
|  |         Text( | ||||||
|  |           (room.type == 1 && room.name == null) | ||||||
|  |               ? room.members!.map((e) => e.account.nick).join(', ') | ||||||
|  |               : room.name!, | ||||||
|  |         ).fontSize(15), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     Widget compactHeaderWidget() => Row( | ||||||
|  |       spacing: 8, | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |       children: [ | ||||||
|  |         SizedBox( | ||||||
|  |           height: 26, | ||||||
|  |           width: 26, | ||||||
|  |           child: | ||||||
|  |               (room.type == 1 && room.picture?.id == null) | ||||||
|  |                   ? SplitAvatarWidget( | ||||||
|  |                     filesId: | ||||||
|  |                         room.members! | ||||||
|  |                             .map((e) => e.account.profile.picture?.id) | ||||||
|  |                             .toList(), | ||||||
|  |                   ) | ||||||
|  |                   : room.picture?.id != null | ||||||
|  |                   ? ProfilePictureWidget( | ||||||
|  |                     fileId: room.picture?.id, | ||||||
|  |                     fallbackIcon: Symbols.chat, | ||||||
|  |                   ) | ||||||
|  |                   : CircleAvatar( | ||||||
|  |                     child: Text( | ||||||
|  |                       room.name![0].toUpperCase(), | ||||||
|  |                       style: const TextStyle(fontSize: 12), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |         ), | ||||||
|  |         Text( | ||||||
|  |           (room.type == 1 && room.name == null) | ||||||
|  |               ? room.members!.map((e) => e.account.nick).join(', ') | ||||||
|  |               : room.name!, | ||||||
|  |         ).fontSize(19), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: !compactHeader ? const Center(child: PageBackButton()) : null, | ||||||
|  |         automaticallyImplyLeading: false, | ||||||
|  |         toolbarHeight: compactHeader ? null : 64, | ||||||
|  |         title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(), | ||||||
|  |         actions: [ | ||||||
|  |           IconButton( | ||||||
|  |             icon: const Icon(Icons.more_vert), | ||||||
|  |             onPressed: () { | ||||||
|  |               context.pushNamed('chatDetail', pathParameters: {'id': id}); | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|  |           const Gap(8), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |       body: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |         children: [ | ||||||
|  |           Expanded( | ||||||
|  |             child: messages.when( | ||||||
|  |               data: | ||||||
|  |                   (messageList) => | ||||||
|  |                       messageList.isEmpty | ||||||
|  |                           ? Center(child: Text('No messages yet'.tr())) | ||||||
|  |                           : chatMessageListWidget(messageList), | ||||||
|  |               loading: () => const Center(child: CircularProgressIndicator()), | ||||||
|  |               error: | ||||||
|  |                   (error, _) => ResponseErrorWidget( | ||||||
|  |                     error: error, | ||||||
|  |                     onRetry: () => messagesNotifier.loadInitial(), | ||||||
|  |                   ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           // Join button at the bottom for public rooms | ||||||
|  |           Container( | ||||||
|  |             padding: const EdgeInsets.all(16), | ||||||
|  |             child: FilledButton.tonalIcon( | ||||||
|  |               onPressed: () async { | ||||||
|  |                 try { | ||||||
|  |                   showLoadingModal(context); | ||||||
|  |                   final apiClient = ref.read(apiClientProvider); | ||||||
|  |                   await apiClient.post('/sphere/chat/${room.id}/members/me'); | ||||||
|  |                   ref.invalidate(chatroomIdentityProvider(id)); | ||||||
|  |                 } catch (err) { | ||||||
|  |                   showErrorAlert(err); | ||||||
|  |                 } finally { | ||||||
|  |                   if (context.mounted) hideLoadingModal(context); | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               label: Text('chatJoin').tr(), | ||||||
|  |               icon: const Icon(Icons.add), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -21,6 +21,7 @@ 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'; | ||||||
| import 'package:island/pods/database.dart'; | import 'package:island/pods/database.dart'; | ||||||
|  | import 'package:island/screens/chat/search_messages.dart'; | ||||||
|  |  | ||||||
| part 'room_detail.freezed.dart'; | part 'room_detail.freezed.dart'; | ||||||
| part 'room_detail.g.dart'; | part 'room_detail.g.dart'; | ||||||
| @@ -401,11 +402,17 @@ class ChatDetailScreen extends HookConsumerWidget { | |||||||
|                                           ), |                                           ), | ||||||
|                                         ), |                                         ), | ||||||
|                                   ), |                                   ), | ||||||
|                                   onTap: () { |                                   onTap: () async { | ||||||
|                                     context.pushNamed( |                                     final result = await context.pushNamed( | ||||||
|                                       'searchMessages', |                                       'searchMessages', | ||||||
|                                       pathParameters: {'id': id}, |                                       pathParameters: {'id': id}, | ||||||
|                                     ); |                                     ); | ||||||
|  |                                     if (result is SearchMessagesResult) { | ||||||
|  |                                       // Navigate back to room screen with message to jump to | ||||||
|  |                                       if (context.mounted) { | ||||||
|  |                                         context.pop(result.messageId); | ||||||
|  |                                       } | ||||||
|  |                                     } | ||||||
|                                   }, |                                   }, | ||||||
|                                 ), |                                 ), | ||||||
|                               ], |                               ], | ||||||
|   | |||||||
| @@ -1,13 +1,20 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package: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/screens/chat/room.dart'; | import 'package:island/pods/messages_notifier.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:island/widgets/chat/message_item.dart'; | import 'package:island/widgets/chat/message_list_tile.dart'; | ||||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||||
| import 'package:super_sliver_list/super_sliver_list.dart'; | import 'package:super_sliver_list/super_sliver_list.dart'; | ||||||
| 
 | 
 | ||||||
|  | // Class to represent the result when popping from search messages | ||||||
|  | class SearchMessagesResult { | ||||||
|  |   final String messageId; | ||||||
|  |   const SearchMessagesResult(this.messageId); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| class SearchMessagesScreen extends HookConsumerWidget { | class SearchMessagesScreen extends HookConsumerWidget { | ||||||
|   final String roomId; |   final String roomId; | ||||||
| 
 | 
 | ||||||
| @@ -112,24 +119,24 @@ class SearchMessagesScreen extends HookConsumerWidget { | |||||||
|                           ? Center(child: Text('noMessagesFound'.tr())) |                           ? Center(child: Text('noMessagesFound'.tr())) | ||||||
|                           : SuperListView.builder( |                           : SuperListView.builder( | ||||||
|                             padding: const EdgeInsets.symmetric(vertical: 16), |                             padding: const EdgeInsets.symmetric(vertical: 16), | ||||||
|                             reverse: true, // Show newest messages at the bottom |                             reverse: false, // Show newest messages at the top | ||||||
|                             itemCount: messageList.length, |                             itemCount: messageList.length, | ||||||
|                             itemBuilder: (context, index) { |                             itemBuilder: (context, index) { | ||||||
|                               final message = messageList[index]; |                               final message = messageList[index]; | ||||||
|                               // Simplified MessageItem for search results, no grouping logic |                               return MessageListTile( | ||||||
|                               return MessageItem( |  | ||||||
|                                 message: message, |                                 message: message, | ||||||
|                                 isCurrentUser: |                                 onJump: (messageId) { | ||||||
|                                     false, // Or determine based on actual user |                                   // Return the search result and pop back to room detail | ||||||
|                                 onAction: null, |                                   context.pop(SearchMessagesResult(messageId)); | ||||||
|                                 onJump: (_) {}, |                                 }, | ||||||
|                                 progress: null, |  | ||||||
|                                 showAvatar: true, |  | ||||||
|                               ); |                               ); | ||||||
|                             }, |                             }, | ||||||
|                           ), |                           ), | ||||||
|               loading: () => const Center(child: CircularProgressIndicator()), |               loading: () => const Center(child: CircularProgressIndicator()), | ||||||
|               error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))), |               error: | ||||||
|  |                   (error, _) => Center( | ||||||
|  |                     child: Text('errorGeneric'.tr(args: [error.toString()])), | ||||||
|  |                   ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
| @@ -78,6 +78,7 @@ class EditPublisherScreen extends HookConsumerWidget { | |||||||
|       result = await cropImage( |       result = await cropImage( | ||||||
|         context, |         context, | ||||||
|         image: result, |         image: result, | ||||||
|  |         replacePath: true, | ||||||
|         allowedAspectRatios: [ |         allowedAspectRatios: [ | ||||||
|           if (position == 'background') |           if (position == 'background') | ||||||
|             CropAspectRatio(height: 7, width: 16) |             CropAspectRatio(height: 7, width: 16) | ||||||
| @@ -98,7 +99,7 @@ class EditPublisherScreen extends HookConsumerWidget { | |||||||
|         final token = await getToken(ref.watch(tokenProvider)); |         final token = await getToken(ref.watch(tokenProvider)); | ||||||
|         if (token == null) throw ArgumentError('Token is null'); |         if (token == null) throw ArgumentError('Token is null'); | ||||||
|         final cloudFile = |         final cloudFile = | ||||||
|             await putMediaToCloud( |             await putFileToCloud( | ||||||
|               fileData: UniversalFile( |               fileData: UniversalFile( | ||||||
|                 data: result, |                 data: result, | ||||||
|                 type: UniversalFileType.image, |                 type: UniversalFileType.image, | ||||||
|   | |||||||
| @@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget { | |||||||
|         final token = await getToken(ref.watch(tokenProvider)); |         final token = await getToken(ref.watch(tokenProvider)); | ||||||
|         if (token == null) throw ArgumentError('Token is null'); |         if (token == null) throw ArgumentError('Token is null'); | ||||||
|         final cloudFile = |         final cloudFile = | ||||||
|             await putMediaToCloud( |             await putFileToCloud( | ||||||
|               fileData: UniversalFile( |               fileData: UniversalFile( | ||||||
|                 data: result, |                 data: result, | ||||||
|                 type: UniversalFileType.image, |                 type: UniversalFileType.image, | ||||||
|   | |||||||
| @@ -127,7 +127,7 @@ class EditBotScreen extends HookConsumerWidget { | |||||||
|         final token = await getToken(ref.watch(tokenProvider)); |         final token = await getToken(ref.watch(tokenProvider)); | ||||||
|         if (token == null) throw ArgumentError('Token is null'); |         if (token == null) throw ArgumentError('Token is null'); | ||||||
|         final cloudFile = |         final cloudFile = | ||||||
|             await putMediaToCloud( |             await putFileToCloud( | ||||||
|               fileData: UniversalFile( |               fileData: UniversalFile( | ||||||
|                 data: result, |                 data: result, | ||||||
|                 type: UniversalFileType.image, |                 type: UniversalFileType.image, | ||||||
|   | |||||||
							
								
								
									
										556
									
								
								lib/screens/files/file_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										556
									
								
								lib/screens/files/file_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,556 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:fl_chart/fl_chart.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/file.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/pods/file_pool.dart'; | ||||||
|  | import 'package:island/utils/format.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
|  | import 'package:island/widgets/content/file_info_sheet.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|  | import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | part 'file_list.g.dart'; | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | class CloudFileListNotifier extends _$CloudFileListNotifier | ||||||
|  |     with CursorPagingNotifierMixin<SnCloudFile> { | ||||||
|  |   String? _poolId; | ||||||
|  |   bool _includeRecycled = false; | ||||||
|  |  | ||||||
|  |   void setFilters(String? poolId, bool includeRecycled) { | ||||||
|  |     _poolId = poolId; | ||||||
|  |     _includeRecycled = includeRecycled; | ||||||
|  |     ref.invalidateSelf(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async { | ||||||
|  |     final client = ref.read(apiClientProvider); | ||||||
|  |     final offset = cursor == null ? 0 : int.parse(cursor); | ||||||
|  |     final take = 20; | ||||||
|  |  | ||||||
|  |     final queryParameters = <String, dynamic>{'offset': offset, 'take': take}; | ||||||
|  |  | ||||||
|  |     // Add filter parameters | ||||||
|  |     if (_poolId != null) { | ||||||
|  |       queryParameters['pool'] = _poolId!; | ||||||
|  |     } | ||||||
|  |     if (_includeRecycled) { | ||||||
|  |       queryParameters['recycled'] = 'true'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final response = await client.get( | ||||||
|  |       '/drive/files/me', | ||||||
|  |       queryParameters: queryParameters, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final List<SnCloudFile> items = | ||||||
|  |         (response.data as List) | ||||||
|  |             .map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>)) | ||||||
|  |             .toList(); | ||||||
|  |     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||||
|  |  | ||||||
|  |     final hasMore = offset + items.length < total; | ||||||
|  |     final nextCursor = hasMore ? (offset + items.length).toString() : null; | ||||||
|  |  | ||||||
|  |     return CursorPagingData( | ||||||
|  |       items: items, | ||||||
|  |       hasMore: hasMore, | ||||||
|  |       nextCursor: nextCursor, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<Map<String, dynamic>?> billingUsage(Ref ref) async { | ||||||
|  |   final client = ref.read(apiClientProvider); | ||||||
|  |   final response = await client.get('/drive/billing/usage'); | ||||||
|  |   return response.data; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<Map<String, dynamic>?> billingQuota(Ref ref) async { | ||||||
|  |   final client = ref.read(apiClientProvider); | ||||||
|  |   final response = await client.get('/drive/billing/quota'); | ||||||
|  |   return response.data; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class FileListScreen extends HookConsumerWidget { | ||||||
|  |   const FileListScreen({super.key}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     // Filter state | ||||||
|  |     final selectedPool = useState<String?>(null); | ||||||
|  |     final includeRecycled = useState(false); | ||||||
|  |  | ||||||
|  |     final usageAsync = ref.watch(billingUsageProvider); | ||||||
|  |     final quotaAsync = ref.watch(billingQuotaProvider); | ||||||
|  |  | ||||||
|  |     // Update notifier filters when state changes | ||||||
|  |     useEffect(() { | ||||||
|  |       final notifier = ref.read(cloudFileListNotifierProvider.notifier); | ||||||
|  |       notifier.setFilters(selectedPool.value, includeRecycled.value); | ||||||
|  |       return null; | ||||||
|  |     }, [selectedPool.value, includeRecycled.value]); | ||||||
|  |  | ||||||
|  |     return AppScaffold( | ||||||
|  |       appBar: AppBar(title: Text('Files'), leading: const PageBackButton()), | ||||||
|  |       body: usageAsync.when( | ||||||
|  |         data: | ||||||
|  |             (usage) => quotaAsync.when( | ||||||
|  |               data: | ||||||
|  |                   (quota) => _buildQuotaUI( | ||||||
|  |                     usage, | ||||||
|  |                     quota, | ||||||
|  |                     ref, | ||||||
|  |                     selectedPool, | ||||||
|  |                     includeRecycled, | ||||||
|  |                   ), | ||||||
|  |               loading: () => const Center(child: CircularProgressIndicator()), | ||||||
|  |               error: (e, _) => Center(child: Text('Error loading quota')), | ||||||
|  |             ), | ||||||
|  |         loading: () => const Center(child: CircularProgressIndicator()), | ||||||
|  |         error: (e, _) => Center(child: Text('Error loading usage')), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildQuotaUI( | ||||||
|  |     Map<String, dynamic>? usage, | ||||||
|  |     Map<String, dynamic>? quota, | ||||||
|  |     WidgetRef ref, | ||||||
|  |     ValueNotifier<String?> selectedPool, | ||||||
|  |     ValueNotifier<bool> includeRecycled, | ||||||
|  |   ) { | ||||||
|  |     if (usage == null) return const SizedBox.shrink(); | ||||||
|  |     return CustomScrollView( | ||||||
|  |       slivers: [ | ||||||
|  |         const SliverGap(8), | ||||||
|  |         SliverToBoxAdapter( | ||||||
|  |           child: Column( | ||||||
|  |             children: [ | ||||||
|  |               Row( | ||||||
|  |                 children: [ | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'All Uploads', | ||||||
|  |                       '${((usage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB', | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'All Files', | ||||||
|  |                       '${usage['total_file_count']}', | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Row( | ||||||
|  |                 children: [ | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'Quota', | ||||||
|  |                       '${usage['total_quota']} MiB', | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'Used Quota', | ||||||
|  |                       '${((usage['used_quota'] as num) / (usage['total_quota'] as num) * 100).toStringAsFixed(2)}%', | ||||||
|  |                       progress: | ||||||
|  |                           (usage['used_quota'] as num) / | ||||||
|  |                           (usage['total_quota'] as num), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: 8), | ||||||
|  |         ), | ||||||
|  |         SliverToBoxAdapter( | ||||||
|  |           child: Row( | ||||||
|  |             children: [ | ||||||
|  |               Expanded( | ||||||
|  |                 child: Card( | ||||||
|  |                   child: Padding( | ||||||
|  |                     padding: const EdgeInsets.all(16), | ||||||
|  |                     child: Column( | ||||||
|  |                       children: [ | ||||||
|  |                         const Text('Pool Usage'), | ||||||
|  |                         SizedBox( | ||||||
|  |                           height: 200, | ||||||
|  |                           child: PieChart(_buildPoolChartData(usage)), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               Expanded( | ||||||
|  |                 child: Card( | ||||||
|  |                   child: Padding( | ||||||
|  |                     padding: const EdgeInsets.all(16), | ||||||
|  |                     child: Column( | ||||||
|  |                       children: [ | ||||||
|  |                         const Text('Verbose Quota'), | ||||||
|  |                         SizedBox( | ||||||
|  |                           height: 200, | ||||||
|  |                           child: PieChart(_buildQuotaChartData(quota)), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: 8), | ||||||
|  |         ), | ||||||
|  |         const SliverGap(8), | ||||||
|  |         SliverToBoxAdapter( | ||||||
|  |           child: _buildFilters(ref, selectedPool, includeRecycled), | ||||||
|  |         ), | ||||||
|  |         const SliverGap(8), | ||||||
|  |         PagingHelperSliverView( | ||||||
|  |           provider: cloudFileListNotifierProvider, | ||||||
|  |           futureRefreshable: cloudFileListNotifierProvider.future, | ||||||
|  |           notifierRefreshable: cloudFileListNotifierProvider.notifier, | ||||||
|  |           contentBuilder: | ||||||
|  |               (data, widgetCount, endItemView) => SliverList.builder( | ||||||
|  |                 itemCount: widgetCount, | ||||||
|  |                 itemBuilder: (context, index) { | ||||||
|  |                   if (index == widgetCount - 1) { | ||||||
|  |                     return endItemView; | ||||||
|  |                   } | ||||||
|  |  | ||||||
|  |                   final item = data.items[index]; | ||||||
|  |                   final itemType = item.mimeType?.split('/').firstOrNull; | ||||||
|  |                   return ListTile( | ||||||
|  |                     leading: ClipRRect( | ||||||
|  |                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |                       child: SizedBox( | ||||||
|  |                         height: 48, | ||||||
|  |                         width: 48, | ||||||
|  |                         child: switch (itemType) { | ||||||
|  |                           'image' => CloudImageWidget(file: item), | ||||||
|  |                           'audio' => | ||||||
|  |                             const Icon(Symbols.audio_file, fill: 1).center(), | ||||||
|  |                           'video' => | ||||||
|  |                             const Icon(Symbols.video_file, fill: 1).center(), | ||||||
|  |                           _ => | ||||||
|  |                             const Icon(Symbols.body_system, fill: 1).center(), | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     title: | ||||||
|  |                         item.name.isEmpty | ||||||
|  |                             ? Text('untitled').tr().italic() | ||||||
|  |                             : Text( | ||||||
|  |                               item.name, | ||||||
|  |                               maxLines: 1, | ||||||
|  |                               overflow: TextOverflow.ellipsis, | ||||||
|  |                             ), | ||||||
|  |                     subtitle: Text(formatFileSize(item.size)), | ||||||
|  |                     onTap: () { | ||||||
|  |                       showModalBottomSheet( | ||||||
|  |                         useRootNavigator: true, | ||||||
|  |                         context: context, | ||||||
|  |                         isScrollControlled: true, | ||||||
|  |                         builder: (context) => FileInfoSheet(item: item), | ||||||
|  |                       ); | ||||||
|  |                     }, | ||||||
|  |                     trailing: IconButton( | ||||||
|  |                       icon: const Icon(Symbols.delete), | ||||||
|  |                       onPressed: () async { | ||||||
|  |                         final confirmed = await showConfirmAlert( | ||||||
|  |                           'confirmDeleteFile'.tr(), | ||||||
|  |                           'deleteFile'.tr(), | ||||||
|  |                         ); | ||||||
|  |                         if (!confirmed) return; | ||||||
|  |  | ||||||
|  |                         if (context.mounted) showLoadingModal(context); | ||||||
|  |                         try { | ||||||
|  |                           final client = ref.read(apiClientProvider); | ||||||
|  |                           await client.delete('/drive/files/${item.id}'); | ||||||
|  |                           ref.invalidate(cloudFileListNotifierProvider); | ||||||
|  |                         } catch (e) { | ||||||
|  |                           showSnackBar('failedToDeleteFile'.tr()); | ||||||
|  |                         } finally { | ||||||
|  |                           if (context.mounted) hideLoadingModal(context); | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   PieChartData _buildPoolChartData(Map<String, dynamic> usage) { | ||||||
|  |     final pools = usage['pool_usages'] as List<dynamic>; | ||||||
|  |     final colors = [ | ||||||
|  |       Colors.blue, | ||||||
|  |       Colors.green, | ||||||
|  |       Colors.orange, | ||||||
|  |       Colors.red, | ||||||
|  |       Colors.purple, | ||||||
|  |     ]; | ||||||
|  |     return PieChartData( | ||||||
|  |       sections: | ||||||
|  |           pools.asMap().entries.map((entry) { | ||||||
|  |             final pool = entry.value as Map<String, dynamic>; | ||||||
|  |             final title = pool['pool_name'] as String; | ||||||
|  |             final truncatedTitle = | ||||||
|  |                 title.length > 8 ? '${title.substring(0, 8)}...' : title; | ||||||
|  |             return PieChartSectionData( | ||||||
|  |               value: (pool['usage_bytes'] as num).toDouble(), | ||||||
|  |               title: truncatedTitle, | ||||||
|  |               color: colors[entry.key % colors.length], | ||||||
|  |               radius: 60, | ||||||
|  |               titleStyle: const TextStyle( | ||||||
|  |                 fontSize: 12, | ||||||
|  |                 color: Colors.white, | ||||||
|  |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|  |             ); | ||||||
|  |           }).toList(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) { | ||||||
|  |     if (quota == null) return PieChartData(sections: []); | ||||||
|  |     return PieChartData( | ||||||
|  |       sections: [ | ||||||
|  |         PieChartSectionData( | ||||||
|  |           value: (quota['based_quota'] as num).toDouble(), | ||||||
|  |           title: 'Base', | ||||||
|  |           color: Colors.green, | ||||||
|  |           radius: 60, | ||||||
|  |           titleStyle: const TextStyle( | ||||||
|  |             fontSize: 12, | ||||||
|  |             color: Colors.white, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         PieChartSectionData( | ||||||
|  |           value: (quota['extra_quota'] as num).toDouble(), | ||||||
|  |           title: 'Extra', | ||||||
|  |           color: Colors.orange, | ||||||
|  |           radius: 60, | ||||||
|  |           titleStyle: const TextStyle( | ||||||
|  |             fontSize: 12, | ||||||
|  |             color: Colors.white, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildFilters( | ||||||
|  |     WidgetRef ref, | ||||||
|  |     ValueNotifier<String?> selectedPool, | ||||||
|  |     ValueNotifier<bool> includeRecycled, | ||||||
|  |   ) { | ||||||
|  |     final poolsAsync = ref.watch(poolsProvider); | ||||||
|  |  | ||||||
|  |     return Card( | ||||||
|  |       child: Padding( | ||||||
|  |         padding: const EdgeInsets.all(16), | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             Text( | ||||||
|  |               'filters'.tr(), | ||||||
|  |               style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), | ||||||
|  |             ), | ||||||
|  |             const Gap(16), | ||||||
|  |             LayoutBuilder( | ||||||
|  |               builder: (context, constraints) { | ||||||
|  |                 final isWide = constraints.maxWidth > 600; | ||||||
|  |                 return isWide | ||||||
|  |                     ? Row( | ||||||
|  |                       children: [ | ||||||
|  |                         Expanded( | ||||||
|  |                           flex: 2, | ||||||
|  |                           child: poolsAsync.when( | ||||||
|  |                             data: | ||||||
|  |                                 (pools) => DropdownButtonFormField<String?>( | ||||||
|  |                                   value: selectedPool.value, | ||||||
|  |                                   decoration: InputDecoration( | ||||||
|  |                                     labelText: 'Pool', | ||||||
|  |                                     border: const OutlineInputBorder(), | ||||||
|  |                                   ), | ||||||
|  |                                   items: [ | ||||||
|  |                                     DropdownMenuItem<String?>( | ||||||
|  |                                       value: null, | ||||||
|  |                                       child: Text('allPools'.tr()), | ||||||
|  |                                     ), | ||||||
|  |                                     ...pools.map( | ||||||
|  |                                       (pool) => DropdownMenuItem<String?>( | ||||||
|  |                                         value: pool.id, | ||||||
|  |                                         child: Text(pool.name), | ||||||
|  |                                       ), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|  |                                   onChanged: | ||||||
|  |                                       (value) => selectedPool.value = value, | ||||||
|  |                                 ), | ||||||
|  |                             loading: () => const CircularProgressIndicator(), | ||||||
|  |                             error: (e, _) => const Text('Error loading pools'), | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         const Gap(8), | ||||||
|  |                         Expanded( | ||||||
|  |                           child: Row( | ||||||
|  |                             children: [ | ||||||
|  |                               Text('includeRecycled'.tr()), | ||||||
|  |                               const Gap(8), | ||||||
|  |                               Switch( | ||||||
|  |                                 value: includeRecycled.value, | ||||||
|  |                                 onChanged: | ||||||
|  |                                     (value) => includeRecycled.value = value, | ||||||
|  |                                 padding: EdgeInsets.zero, | ||||||
|  |                               ), | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         const Gap(16), | ||||||
|  |                         IconButton( | ||||||
|  |                           icon: const Icon(Symbols.delete_sweep), | ||||||
|  |                           tooltip: 'deleteRecycledFiles'.tr(), | ||||||
|  |                           onPressed: | ||||||
|  |                               includeRecycled.value | ||||||
|  |                                   ? () => _deleteRecycledFiles(ref) | ||||||
|  |                                   : null, | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ) | ||||||
|  |                     : Column( | ||||||
|  |                       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                       children: [ | ||||||
|  |                         poolsAsync.when( | ||||||
|  |                           data: | ||||||
|  |                               (pools) => DropdownButtonFormField<String?>( | ||||||
|  |                                 value: selectedPool.value, | ||||||
|  |                                 decoration: const InputDecoration( | ||||||
|  |                                   labelText: 'Pool', | ||||||
|  |                                   border: OutlineInputBorder(), | ||||||
|  |                                 ), | ||||||
|  |                                 items: [ | ||||||
|  |                                   DropdownMenuItem<String?>( | ||||||
|  |                                     value: null, | ||||||
|  |                                     child: Text('allPools'.tr()), | ||||||
|  |                                   ), | ||||||
|  |                                   ...pools.map( | ||||||
|  |                                     (pool) => DropdownMenuItem<String?>( | ||||||
|  |                                       value: pool.id, | ||||||
|  |                                       child: Text(pool.name), | ||||||
|  |                                     ), | ||||||
|  |                                   ), | ||||||
|  |                                 ], | ||||||
|  |                                 onChanged: | ||||||
|  |                                     (value) => selectedPool.value = value, | ||||||
|  |                               ), | ||||||
|  |                           loading: () => const CircularProgressIndicator(), | ||||||
|  |                           error: (e, _) => const Text('Error loading pools'), | ||||||
|  |                         ), | ||||||
|  |                         const Gap(16), | ||||||
|  |                         Row( | ||||||
|  |                           children: [ | ||||||
|  |                             Text('includeRecycled'.tr()), | ||||||
|  |                             const Gap(8), | ||||||
|  |                             Switch( | ||||||
|  |                               value: includeRecycled.value, | ||||||
|  |                               onChanged: | ||||||
|  |                                   (value) => includeRecycled.value = value, | ||||||
|  |                             ), | ||||||
|  |                             const Spacer(), | ||||||
|  |                             IconButton( | ||||||
|  |                               icon: const Icon(Symbols.delete_sweep), | ||||||
|  |                               tooltip: 'deleteRecycledFiles'.tr(), | ||||||
|  |                               onPressed: | ||||||
|  |                                   includeRecycled.value | ||||||
|  |                                       ? () => _deleteRecycledFiles(ref) | ||||||
|  |                                       : null, | ||||||
|  |                             ), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ).padding(horizontal: 8); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> _deleteRecycledFiles(WidgetRef ref) async { | ||||||
|  |     final confirmed = await showConfirmAlert( | ||||||
|  |       'confirmDeleteRecycledFiles'.tr(), | ||||||
|  |       'deleteRecycledFiles'.tr(), | ||||||
|  |     ); | ||||||
|  |     if (!confirmed) return; | ||||||
|  |  | ||||||
|  |     if (ref.context.mounted) showLoadingModal(ref.context); | ||||||
|  |     try { | ||||||
|  |       final client = ref.read(apiClientProvider); | ||||||
|  |       await client.delete('/drive/files/recycled'); | ||||||
|  |       ref.invalidate(cloudFileListNotifierProvider); | ||||||
|  |       showSnackBar('recycledFilesDeleted'.tr()); | ||||||
|  |     } catch (e) { | ||||||
|  |       showSnackBar('failedToDeleteRecycledFiles'.tr()); | ||||||
|  |     } finally { | ||||||
|  |       if (ref.context.mounted) hideLoadingModal(ref.context); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildStatCard(String label, String value, {double? progress}) { | ||||||
|  |     return Card( | ||||||
|  |       child: Padding( | ||||||
|  |         padding: const EdgeInsets.all(16), | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |           children: [ | ||||||
|  |             Text(label, style: const TextStyle(fontSize: 14)), | ||||||
|  |             Row( | ||||||
|  |               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |               children: [ | ||||||
|  |                 Text( | ||||||
|  |                   value, | ||||||
|  |                   style: const TextStyle( | ||||||
|  |                     fontSize: 24, | ||||||
|  |                     fontWeight: FontWeight.bold, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 if (progress != null) ...[ | ||||||
|  |                   const SizedBox(height: 8), | ||||||
|  |                   SizedBox( | ||||||
|  |                     width: 28, | ||||||
|  |                     height: 28, | ||||||
|  |                     child: CircularProgressIndicator(value: progress), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								lib/screens/files/file_list.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								lib/screens/files/file_list.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
|  |  | ||||||
|  | part of 'file_list.dart'; | ||||||
|  |  | ||||||
|  | // ************************************************************************** | ||||||
|  | // RiverpodGenerator | ||||||
|  | // ************************************************************************** | ||||||
|  |  | ||||||
|  | String _$billingUsageHash() => r'58d8bc774868d60781574c85d6b25869a79c57aa'; | ||||||
|  |  | ||||||
|  | /// See also [billingUsage]. | ||||||
|  | @ProviderFor(billingUsage) | ||||||
|  | final billingUsageProvider = | ||||||
|  |     AutoDisposeFutureProvider<Map<String, dynamic>?>.internal( | ||||||
|  |       billingUsage, | ||||||
|  |       name: r'billingUsageProvider', | ||||||
|  |       debugGetCreateSourceHash: | ||||||
|  |           const bool.fromEnvironment('dart.vm.product') | ||||||
|  |               ? null | ||||||
|  |               : _$billingUsageHash, | ||||||
|  |       dependencies: null, | ||||||
|  |       allTransitiveDependencies: null, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | typedef BillingUsageRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>; | ||||||
|  | String _$billingQuotaHash() => r'4ec5d728e439015800abb2d0d673b5a7329cc654'; | ||||||
|  |  | ||||||
|  | /// See also [billingQuota]. | ||||||
|  | @ProviderFor(billingQuota) | ||||||
|  | final billingQuotaProvider = | ||||||
|  |     AutoDisposeFutureProvider<Map<String, dynamic>?>.internal( | ||||||
|  |       billingQuota, | ||||||
|  |       name: r'billingQuotaProvider', | ||||||
|  |       debugGetCreateSourceHash: | ||||||
|  |           const bool.fromEnvironment('dart.vm.product') | ||||||
|  |               ? null | ||||||
|  |               : _$billingQuotaHash, | ||||||
|  |       dependencies: null, | ||||||
|  |       allTransitiveDependencies: null, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>; | ||||||
|  | String _$cloudFileListNotifierHash() => | ||||||
|  |     r'22c45a8ea23147a3835ba870ad2f0bb833f853ea'; | ||||||
|  |  | ||||||
|  | /// See also [CloudFileListNotifier]. | ||||||
|  | @ProviderFor(CloudFileListNotifier) | ||||||
|  | final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< | ||||||
|  |   CloudFileListNotifier, | ||||||
|  |   CursorPagingData<SnCloudFile> | ||||||
|  | >.internal( | ||||||
|  |   CloudFileListNotifier.new, | ||||||
|  |   name: r'cloudFileListNotifierProvider', | ||||||
|  |   debugGetCreateSourceHash: | ||||||
|  |       const bool.fromEnvironment('dart.vm.product') | ||||||
|  |           ? null | ||||||
|  |           : _$cloudFileListNotifierHash, | ||||||
|  |   dependencies: null, | ||||||
|  |   allTransitiveDependencies: null, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | typedef _$CloudFileListNotifier = | ||||||
|  |     AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>; | ||||||
|  | // ignore_for_file: type=lint | ||||||
|  | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||||
| @@ -39,7 +39,7 @@ class NotificationUnreadCountNotifier | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final client = ref.read(apiClientProvider); |       final client = ref.read(apiClientProvider); | ||||||
|       final response = await client.get('/pusher/notifications/count'); |       final response = await client.get('/ring/notifications/count'); | ||||||
|       return (response.data as num).toInt(); |       return (response.data as num).toInt(); | ||||||
|     } catch (_) { |     } catch (_) { | ||||||
|       return 0; |       return 0; | ||||||
| @@ -89,7 +89,7 @@ class NotificationListNotifier extends _$NotificationListNotifier | |||||||
|     final queryParams = {'offset': offset, 'take': _pageSize}; |     final queryParams = {'offset': offset, 'take': _pageSize}; | ||||||
|  |  | ||||||
|     final response = await client.get( |     final response = await client.get( | ||||||
|       '/pusher/notifications', |       '/ring/notifications', | ||||||
|       queryParameters: queryParams, |       queryParameters: queryParams, | ||||||
|     ); |     ); | ||||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); |     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||||
| @@ -121,7 +121,7 @@ class NotificationScreen extends HookConsumerWidget { | |||||||
|     Future<void> markAllRead() async { |     Future<void> markAllRead() async { | ||||||
|       showLoadingModal(context); |       showLoadingModal(context); | ||||||
|       final apiClient = ref.watch(apiClientProvider); |       final apiClient = ref.watch(apiClientProvider); | ||||||
|       await apiClient.post('/pusher/notifications/all/read'); |       await apiClient.post('/ring/notifications/all/read'); | ||||||
|       if (!context.mounted) return; |       if (!context.mounted) return; | ||||||
|       hideLoadingModal(context); |       hideLoadingModal(context); | ||||||
|       ref.invalidate(notificationListNotifierProvider); |       ref.invalidate(notificationListNotifierProvider); | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ part of 'notification.dart'; | |||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$notificationUnreadCountNotifierHash() => | String _$notificationUnreadCountNotifierHash() => | ||||||
|     r'0763b66bd64e5a9b7c317887e109ab367515dfa4'; |     r'08c773809958d96a7ce82acf04af1f9e0b23e119'; | ||||||
|  |  | ||||||
| /// See also [NotificationUnreadCountNotifier]. | /// See also [NotificationUnreadCountNotifier]. | ||||||
| @ProviderFor(NotificationUnreadCountNotifier) | @ProviderFor(NotificationUnreadCountNotifier) | ||||||
| @@ -28,7 +28,7 @@ final notificationUnreadCountNotifierProvider = | |||||||
|  |  | ||||||
| typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>; | typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>; | ||||||
| String _$notificationListNotifierHash() => | String _$notificationListNotifierHash() => | ||||||
|     r'5099466db475bbcf1ab6b514eb072f1dc4c6f930'; |     r'260046e11f45b0d67ab25bcbdc8604890d71ccc7'; | ||||||
|  |  | ||||||
| /// See also [NotificationListNotifier]. | /// See also [NotificationListNotifier]. | ||||||
| @ProviderFor(NotificationListNotifier) | @ProviderFor(NotificationListNotifier) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import 'package:island/screens/creators/publishers.dart'; | |||||||
| import 'package:island/screens/posts/compose_article.dart'; | import 'package:island/screens/posts/compose_article.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/attachment_uploader.dart'; | ||||||
| import 'package:island/widgets/content/attachment_preview.dart'; | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/post/compose_shared.dart'; | import 'package:island/widgets/post/compose_shared.dart'; | ||||||
| @@ -225,8 +226,26 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|           return AttachmentPreview( |           return AttachmentPreview( | ||||||
|             item: state.attachments.value[idx], |             item: state.attachments.value[idx], | ||||||
|             progress: progressMap[idx], |             progress: progressMap[idx], | ||||||
|             onRequestUpload: |             onRequestUpload: () async { | ||||||
|                 () => ComposeLogic.uploadAttachment(ref, state, idx), |               final config = await showModalBottomSheet<AttachmentUploadConfig>( | ||||||
|  |                 context: context, | ||||||
|  |                 isScrollControlled: true, | ||||||
|  |                 builder: | ||||||
|  |                     (context) => AttachmentUploaderSheet( | ||||||
|  |                       ref: ref, | ||||||
|  |                       state: state, | ||||||
|  |                       index: idx, | ||||||
|  |                     ), | ||||||
|  |               ); | ||||||
|  |               if (config != null) { | ||||||
|  |                 await ComposeLogic.uploadAttachment( | ||||||
|  |                   ref, | ||||||
|  |                   state, | ||||||
|  |                   idx, | ||||||
|  |                   poolId: config.poolId, | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|             onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), |             onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||||
|             onUpdate: |             onUpdate: | ||||||
|                 (value) => ComposeLogic.updateAttachment(state, value, idx), |                 (value) => ComposeLogic.updateAttachment(state, value, idx), | ||||||
| @@ -253,8 +272,27 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|                 return AttachmentPreview( |                 return AttachmentPreview( | ||||||
|                   item: state.attachments.value[idx], |                   item: state.attachments.value[idx], | ||||||
|                   progress: progressMap[idx], |                   progress: progressMap[idx], | ||||||
|                   onRequestUpload: |                   onRequestUpload: () async { | ||||||
|                       () => ComposeLogic.uploadAttachment(ref, state, idx), |                     final config = | ||||||
|  |                         await showModalBottomSheet<AttachmentUploadConfig>( | ||||||
|  |                           context: context, | ||||||
|  |                           isScrollControlled: true, | ||||||
|  |                           builder: | ||||||
|  |                               (context) => AttachmentUploaderSheet( | ||||||
|  |                                 ref: ref, | ||||||
|  |                                 state: state, | ||||||
|  |                                 index: idx, | ||||||
|  |                               ), | ||||||
|  |                         ); | ||||||
|  |                     if (config != null) { | ||||||
|  |                       await ComposeLogic.uploadAttachment( | ||||||
|  |                         ref, | ||||||
|  |                         state, | ||||||
|  |                         idx, | ||||||
|  |                         poolId: config.poolId, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|                   onDelete: |                   onDelete: | ||||||
|                       () => ComposeLogic.deleteAttachment(ref, state, idx), |                       () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||||
|                   onUpdate: |                   onUpdate: | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import 'dart:async'; | |||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; |  | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| @@ -12,6 +11,7 @@ import 'package:island/models/post.dart'; | |||||||
| import 'package:island/screens/creators/publishers.dart'; | import 'package:island/screens/creators/publishers.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/attachment_uploader.dart'; | ||||||
| import 'package:island/screens/posts/post_detail.dart'; | import 'package:island/screens/posts/post_detail.dart'; | ||||||
| import 'package:island/widgets/content/attachment_preview.dart'; | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| @@ -346,12 +346,30 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|                                       isCompact: true, |                                       isCompact: true, | ||||||
|                                       item: attachments[idx], |                                       item: attachments[idx], | ||||||
|                                       progress: progressMap[idx], |                                       progress: progressMap[idx], | ||||||
|                                       onRequestUpload: |                                       onRequestUpload: () async { | ||||||
|                                           () => ComposeLogic.uploadAttachment( |                                         final config = | ||||||
|  |                                             await showModalBottomSheet< | ||||||
|  |                                               AttachmentUploadConfig | ||||||
|  |                                             >( | ||||||
|  |                                               context: context, | ||||||
|  |                                               isScrollControlled: true, | ||||||
|  |                                               builder: | ||||||
|  |                                                   (context) => | ||||||
|  |                                                       AttachmentUploaderSheet( | ||||||
|  |                                                         ref: ref, | ||||||
|  |                                                         state: state, | ||||||
|  |                                                         index: idx, | ||||||
|  |                                                       ), | ||||||
|  |                                             ); | ||||||
|  |                                         if (config != null) { | ||||||
|  |                                           await ComposeLogic.uploadAttachment( | ||||||
|                                             ref, |                                             ref, | ||||||
|                                             state, |                                             state, | ||||||
|                                             idx, |                                             idx, | ||||||
|                                           ), |                                             poolId: config.poolId, | ||||||
|  |                                           ); | ||||||
|  |                                         } | ||||||
|  |                                       }, | ||||||
|                                       onUpdate: |                                       onUpdate: | ||||||
|                                           (value) => |                                           (value) => | ||||||
|                                               ComposeLogic.updateAttachment( |                                               ComposeLogic.updateAttachment( | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ import 'package:island/widgets/content/cloud_files.dart'; | |||||||
| import 'package:island/widgets/content/markdown.dart'; | import 'package:island/widgets/content/markdown.dart'; | ||||||
| import 'package:island/widgets/post/post_list.dart'; | import 'package:island/widgets/post/post_list.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:palette_generator/palette_generator.dart'; | import 'package:island/services/color_extraction.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| @@ -278,14 +278,14 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async { | |||||||
|   try { |   try { | ||||||
|     final publisher = await ref.watch(publisherProvider(pubName).future); |     final publisher = await ref.watch(publisherProvider(pubName).future); | ||||||
|     if (publisher.background == null) return null; |     if (publisher.background == null) return null; | ||||||
|     final palette = await PaletteGenerator.fromImageProvider( |     final colors = await ColorExtractionService.getColorsFromImage( | ||||||
|       CloudImageWidget.provider( |       CloudImageWidget.provider( | ||||||
|         fileId: publisher.background!.id, |         fileId: publisher.background!.id, | ||||||
|         serverUrl: ref.watch(serverUrlProvider), |         serverUrl: ref.watch(serverUrlProvider), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|     final dominantColor = palette.dominantColor?.color; |     if (colors.isEmpty) return null; | ||||||
|     if (dominantColor == null) return null; |     final dominantColor = colors.first; | ||||||
|     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; |     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||||
|   } catch (_) { |   } catch (_) { | ||||||
|     return null; |     return null; | ||||||
|   | |||||||
| @@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement | |||||||
| } | } | ||||||
|  |  | ||||||
| String _$publisherAppbarForcegroundColorHash() => | String _$publisherAppbarForcegroundColorHash() => | ||||||
|     r'd781a806a242aea5c1609ec98c97c52fdd9f7db1'; |     r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7'; | ||||||
|  |  | ||||||
| /// See also [publisherAppbarForcegroundColor]. | /// See also [publisherAppbarForcegroundColor]. | ||||||
| @ProviderFor(publisherAppbarForcegroundColor) | @ProviderFor(publisherAppbarForcegroundColor) | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import 'package:island/services/responsive.dart'; | |||||||
| import 'package:island/widgets/account/account_pfc.dart'; | import 'package:island/widgets/account/account_pfc.dart'; | ||||||
| import 'package:island/widgets/account/status.dart'; | import 'package:island/widgets/account/status.dart'; | ||||||
| import 'package:island/widgets/post/post_list.dart'; | import 'package:island/widgets/post/post_list.dart'; | ||||||
| import 'package:palette_generator/palette_generator.dart'; | import 'package:island/services/color_extraction.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| @@ -32,14 +32,14 @@ part 'realm_detail.g.dart'; | |||||||
| Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async { | Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async { | ||||||
|   final realm = await ref.watch(realmProvider(realmSlug).future); |   final realm = await ref.watch(realmProvider(realmSlug).future); | ||||||
|   if (realm?.background == null) return null; |   if (realm?.background == null) return null; | ||||||
|   final palette = await PaletteGenerator.fromImageProvider( |   final colors = await ColorExtractionService.getColorsFromImage( | ||||||
|     CloudImageWidget.provider( |     CloudImageWidget.provider( | ||||||
|       fileId: realm!.background!.id, |       fileId: realm!.background!.id, | ||||||
|       serverUrl: ref.watch(serverUrlProvider), |       serverUrl: ref.watch(serverUrlProvider), | ||||||
|     ), |     ), | ||||||
|   ); |   ); | ||||||
|   final dominantColor = palette.dominantColor?.color; |   if (colors.isEmpty) return null; | ||||||
|   if (dominantColor == null) return null; |   final dominantColor = colors.first; | ||||||
|   return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; |   return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ part of 'realm_detail.dart'; | |||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$realmAppbarForegroundColorHash() => | String _$realmAppbarForegroundColorHash() => | ||||||
|     r'14b5563d861996ea182d0d2db7aa5c2bb3bbaf48'; |     r'8131c047a984318a4cc3fbb5daa5ef0ce44dfae5'; | ||||||
|  |  | ||||||
| /// Copied from Dart SDK | /// Copied from Dart SDK | ||||||
| class _SystemHash { | class _SystemHash { | ||||||
|   | |||||||
| @@ -211,7 +211,7 @@ class EditRealmScreen extends HookConsumerWidget { | |||||||
|         final token = await getToken(ref.watch(tokenProvider)); |         final token = await getToken(ref.watch(tokenProvider)); | ||||||
|         if (token == null) throw ArgumentError('Access token is null'); |         if (token == null) throw ArgumentError('Access token is null'); | ||||||
|         final cloudFile = |         final cloudFile = | ||||||
|             await putMediaToCloud( |             await putFileToCloud( | ||||||
|               fileData: UniversalFile( |               fileData: UniversalFile( | ||||||
|                 data: result, |                 data: result, | ||||||
|                 type: UniversalFileType.image, |                 type: UniversalFileType.image, | ||||||
|   | |||||||
| @@ -12,14 +12,16 @@ import 'package:flutter_hooks/flutter_hooks.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/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/pods/userinfo.dart'; | ||||||
|  | import 'package:island/services/color_extraction.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:palette_generator/palette_generator.dart'; |  | ||||||
| import 'package:path_provider/path_provider.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
|  | import 'package:island/pods/file_pool.dart'; | ||||||
|  |  | ||||||
| class SettingsScreen extends HookConsumerWidget { | class SettingsScreen extends HookConsumerWidget { | ||||||
|   const SettingsScreen({super.key}); |   const SettingsScreen({super.key}); | ||||||
| @@ -33,7 +35,8 @@ class SettingsScreen extends HookConsumerWidget { | |||||||
|     final isDesktop = |     final isDesktop = | ||||||
|         !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); |         !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); | ||||||
|     final isWide = isWideScreen(context); |     final isWide = isWideScreen(context); | ||||||
|  |     final pools = ref.watch(poolsProvider); | ||||||
|  |     final user = ref.watch(userInfoProvider); | ||||||
|     final docBasepath = useState<String?>(null); |     final docBasepath = useState<String?>(null); | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
| @@ -127,6 +130,48 @@ class SettingsScreen extends HookConsumerWidget { | |||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|  |  | ||||||
|  |       // Message display style settings | ||||||
|  |       ListTile( | ||||||
|  |         minLeadingWidth: 48, | ||||||
|  |         title: Text('settingsMessageDisplayStyle').tr(), | ||||||
|  |         contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||||
|  |         leading: const Icon(Symbols.chat), | ||||||
|  |         trailing: DropdownButtonHideUnderline( | ||||||
|  |           child: DropdownButton2<String>( | ||||||
|  |             isExpanded: true, | ||||||
|  |             items: [ | ||||||
|  |               DropdownMenuItem<String>( | ||||||
|  |                 value: 'bubble', | ||||||
|  |                 child: Text('Bubble').fontSize(14), | ||||||
|  |               ), | ||||||
|  |               DropdownMenuItem<String>( | ||||||
|  |                 value: 'column', | ||||||
|  |                 child: Text('Column').fontSize(14), | ||||||
|  |               ), | ||||||
|  |               DropdownMenuItem<String>( | ||||||
|  |                 value: 'compact', | ||||||
|  |                 child: Text('Compact').fontSize(14), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |             value: settings.messageDisplayStyle, | ||||||
|  |             onChanged: (String? value) { | ||||||
|  |               if (value != null) { | ||||||
|  |                 ref | ||||||
|  |                     .read(appSettingsNotifierProvider.notifier) | ||||||
|  |                     .setMessageDisplayStyle(value); | ||||||
|  |                 showSnackBar('settingsApplied'.tr()); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             buttonStyleData: const ButtonStyleData( | ||||||
|  |               padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), | ||||||
|  |               height: 40, | ||||||
|  |               width: 140, | ||||||
|  |             ), | ||||||
|  |             menuItemStyleData: const MenuItemStyleData(height: 40), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |  | ||||||
|       // Color scheme settings |       // Color scheme settings | ||||||
|       ListTile( |       ListTile( | ||||||
|         minLeadingWidth: 48, |         minLeadingWidth: 48, | ||||||
| @@ -293,24 +338,26 @@ class SettingsScreen extends HookConsumerWidget { | |||||||
|               trailing: const Icon(Symbols.chevron_right), |               trailing: const Icon(Symbols.chevron_right), | ||||||
|               onTap: () async { |               onTap: () async { | ||||||
|                 showLoadingModal(context); |                 showLoadingModal(context); | ||||||
|                 final palette = await PaletteGenerator.fromImageProvider( |                 final colors = await ColorExtractionService.getColorsFromImage( | ||||||
|                   FileImage( |                   FileImage( | ||||||
|                     File('${docBasepath.value}/$kAppBackgroundImagePath'), |                     File('${docBasepath.value}/$kAppBackgroundImagePath'), | ||||||
|                   ), |                   ), | ||||||
|                 ); |                 ); | ||||||
|                 if (palette.darkVibrantColor == null || |                 if (colors.isEmpty) { | ||||||
|                     palette.lightVibrantColor == null) { |  | ||||||
|                   if (context.mounted) hideLoadingModal(context); |                   if (context.mounted) hideLoadingModal(context); | ||||||
|                   showErrorAlert( |                   showErrorAlert( | ||||||
|                     'Unable to calculate the domiant color of the background image.', |                     'Unable to calculate the dominant color of the background image.', | ||||||
|                   ); |                   ); | ||||||
|                   return; |                   return; | ||||||
|                 } |                 } | ||||||
|                 if (!context.mounted) return; |                 if (!context.mounted) return; | ||||||
|  |                 final colorScheme = ColorScheme.fromSeed( | ||||||
|  |                   seedColor: colors.first, | ||||||
|  |                 ); | ||||||
|                 final color = |                 final color = | ||||||
|                     MediaQuery.of(context).platformBrightness == Brightness.dark |                     MediaQuery.of(context).platformBrightness == Brightness.dark | ||||||
|                         ? palette.darkVibrantColor!.color |                         ? colorScheme.primary | ||||||
|                         : palette.lightVibrantColor!.color; |                         : colorScheme.primary; | ||||||
|                 ref |                 ref | ||||||
|                     .read(appSettingsNotifierProvider.notifier) |                     .read(appSettingsNotifierProvider.notifier) | ||||||
|                     .setAppColorScheme(color.value); |                     .setAppColorScheme(color.value); | ||||||
| @@ -365,6 +412,71 @@ class SettingsScreen extends HookConsumerWidget { | |||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|  |  | ||||||
|  |       if (user.value != null) | ||||||
|  |         pools.when( | ||||||
|  |           data: (data) { | ||||||
|  |             final validPools = data; | ||||||
|  |             final currentPoolId = resolveDefaultPoolId(ref, data); | ||||||
|  |  | ||||||
|  |             return ListTile( | ||||||
|  |               isThreeLine: true, | ||||||
|  |               minLeadingWidth: 48, | ||||||
|  |               title: Text('settingsDefaultPool').tr(), | ||||||
|  |               contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||||
|  |               leading: const Icon(Symbols.cloud), | ||||||
|  |               subtitle: Text( | ||||||
|  |                 'settingsDefaultPoolHelper'.tr(), | ||||||
|  |                 style: Theme.of(context).textTheme.bodySmall, | ||||||
|  |               ), | ||||||
|  |               trailing: DropdownButtonHideUnderline( | ||||||
|  |                 child: DropdownButton2<String>( | ||||||
|  |                   isExpanded: true, | ||||||
|  |                   items: | ||||||
|  |                       validPools.map((p) { | ||||||
|  |                         return DropdownMenuItem<String>( | ||||||
|  |                           value: p.id, | ||||||
|  |                           child: Tooltip( | ||||||
|  |                             message: p.name, | ||||||
|  |                             child: Text( | ||||||
|  |                               p.name, | ||||||
|  |                               maxLines: 1, | ||||||
|  |                               overflow: TextOverflow.ellipsis, | ||||||
|  |                             ).fontSize(14), | ||||||
|  |                           ), | ||||||
|  |                         ); | ||||||
|  |                       }).toList(), | ||||||
|  |                   value: currentPoolId, | ||||||
|  |                   onChanged: (value) { | ||||||
|  |                     ref | ||||||
|  |                         .read(appSettingsNotifierProvider.notifier) | ||||||
|  |                         .setDefaultPoolId(value); | ||||||
|  |                     showSnackBar('settingsApplied'.tr()); | ||||||
|  |                   }, | ||||||
|  |                   buttonStyleData: const ButtonStyleData( | ||||||
|  |                     padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), | ||||||
|  |                     height: 40, | ||||||
|  |                     width: 120, | ||||||
|  |                   ), | ||||||
|  |                   menuItemStyleData: const MenuItemStyleData(height: 40), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |           loading: | ||||||
|  |               () => const ListTile( | ||||||
|  |                 minLeadingWidth: 48, | ||||||
|  |                 title: Text('Loading pools...'), | ||||||
|  |                 leading: CircularProgressIndicator(), | ||||||
|  |               ), | ||||||
|  |           error: | ||||||
|  |               (err, st) => ListTile( | ||||||
|  |                 minLeadingWidth: 48, | ||||||
|  |                 title: Text('settingsDefaultPool').tr(), | ||||||
|  |                 subtitle: Text('Error: $err'), | ||||||
|  |                 leading: const Icon(Icons.error, color: Colors.red), | ||||||
|  |               ), | ||||||
|  |         ), | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     final behaviorSettings = [ |     final behaviorSettings = [ | ||||||
|   | |||||||
| @@ -48,11 +48,12 @@ class TrayService { | |||||||
|   void handleAction(MenuItem item) { |   void handleAction(MenuItem item) { | ||||||
|     switch (item.key) { |     switch (item.key) { | ||||||
|       case 'show_window': |       case 'show_window': | ||||||
|         if (appWindow.isVisible) { |         () async { | ||||||
|           appWindow.restore(); |  | ||||||
|         } else { |  | ||||||
|         appWindow.show(); |         appWindow.show(); | ||||||
|         } |         appWindow.restore(); | ||||||
|  |         await Future.delayed(const Duration(milliseconds: 32)); | ||||||
|  |         appWindow.show(); | ||||||
|  |         }(); | ||||||
|         break; |         break; | ||||||
|       case 'exit_app': |       case 'exit_app': | ||||||
|         appWindow.close(); |         appWindow.close(); | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								lib/services/color_extraction.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/services/color_extraction.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | import 'package:flutter/widgets.dart'; | ||||||
|  | import 'package:image/image.dart' as img; | ||||||
|  | import 'package:material_color_utilities/material_color_utilities.dart' as mcu; | ||||||
|  |  | ||||||
|  | class ColorExtractionService { | ||||||
|  |   /// Extracts dominant colors from an image provider. | ||||||
|  |   /// Returns a list of colors suitable for UI theming. | ||||||
|  |   static Future<List<Color>> getColorsFromImage(ImageProvider provider) async { | ||||||
|  |     try { | ||||||
|  |       if (provider is FileImage) { | ||||||
|  |         final bytes = await provider.file.readAsBytes(); | ||||||
|  |         final image = img.decodeImage(bytes); | ||||||
|  |         if (image == null) return []; | ||||||
|  |         final Map<int, int> colorToCount = {}; | ||||||
|  |         for (int y = 0; y < image.height; y++) { | ||||||
|  |           for (int x = 0; x < image.width; x++) { | ||||||
|  |             final pixel = image.getPixel(x, y) as int; | ||||||
|  |             final r = (pixel >> 24) & 0xff; | ||||||
|  |             final g = (pixel >> 16) & 0xff; | ||||||
|  |             final b = (pixel >> 8) & 0xff; | ||||||
|  |             final a = pixel & 0xff; | ||||||
|  |             if (a == 0) continue; | ||||||
|  |             final argb = (a << 24) | (r << 16) | (g << 8) | b; | ||||||
|  |             colorToCount[argb] = (colorToCount[argb] ?? 0) + 1; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         final List<int> filteredResults = mcu.Score.score( | ||||||
|  |           colorToCount, | ||||||
|  |           desired: 1, | ||||||
|  |           filter: true, | ||||||
|  |         ); | ||||||
|  |         final List<int> scoredResults = mcu.Score.score( | ||||||
|  |           colorToCount, | ||||||
|  |           desired: 4, | ||||||
|  |           filter: false, | ||||||
|  |         ); | ||||||
|  |         return <dynamic>{ | ||||||
|  |           ...filteredResults, | ||||||
|  |           ...scoredResults, | ||||||
|  |         }.toList().map((argb) => Color(argb)).toList(); | ||||||
|  |       } else { | ||||||
|  |         return []; | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       debugPrint('Error getting colors from image: $e'); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,21 +1,23 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; |  | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
| import 'dart:ui'; | import 'dart:ui'; | ||||||
|  |  | ||||||
| import 'package:croppy/croppy.dart'; | import 'package:croppy/croppy.dart'; | ||||||
| import 'package:cross_file/cross_file.dart'; | import 'package:cross_file/cross_file.dart'; | ||||||
|  | import 'package:dio/dio.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/widgets.dart'; | import 'package:flutter/widgets.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
|  | import 'package:island/services/file_uploader.dart'; | ||||||
| import 'package:native_exif/native_exif.dart'; | import 'package:native_exif/native_exif.dart'; | ||||||
| import 'package:tus_client_dart/tus_client_dart.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
|  |  | ||||||
|  | enum FileUploadMode { generic, mediaSafe } | ||||||
|  |  | ||||||
| Future<XFile?> cropImage( | Future<XFile?> cropImage( | ||||||
|   BuildContext context, { |   BuildContext context, { | ||||||
|   required XFile image, |   required XFile image, | ||||||
|   List<CropAspectRatio?>? allowedAspectRatios, |   List<CropAspectRatio?>? allowedAspectRatios, | ||||||
|   bool replacePath = false, |   bool replacePath = true, | ||||||
| }) async { | }) async { | ||||||
|   final result = await showMaterialImageCropper( |   final result = await showMaterialImageCropper( | ||||||
|     context, |     context, | ||||||
| @@ -40,64 +42,63 @@ Future<XFile?> cropImage( | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| Completer<SnCloudFile?> putMediaToCloud({ | Completer<SnCloudFile?> putFileToCloud({ | ||||||
|   required UniversalFile fileData, |   required UniversalFile fileData, | ||||||
|   required String atk, |   required String atk, | ||||||
|   required String baseUrl, |   required String baseUrl, | ||||||
|  |   String? poolId, | ||||||
|   String? filename, |   String? filename, | ||||||
|   String? mimetype, |   String? mimetype, | ||||||
|  |   FileUploadMode? mode, | ||||||
|   Function(double progress, Duration estimate)? onProgress, |   Function(double progress, Duration estimate)? onProgress, | ||||||
| }) { | }) { | ||||||
|   final completer = Completer<SnCloudFile?>(); |   final completer = Completer<SnCloudFile?>(); | ||||||
|  |  | ||||||
|   // Process the image to remove GPS EXIF data if needed |   final effectiveMode = | ||||||
|   if (fileData.isOnDevice && fileData.type == UniversalFileType.image) { |       mode ?? | ||||||
|  |       (fileData.type == UniversalFileType.file | ||||||
|  |           ? FileUploadMode.generic | ||||||
|  |           : FileUploadMode.mediaSafe); | ||||||
|  |  | ||||||
|  |   if (effectiveMode == FileUploadMode.mediaSafe && | ||||||
|  |       fileData.isOnDevice && | ||||||
|  |       fileData.type == UniversalFileType.image) { | ||||||
|     final data = fileData.data; |     final data = fileData.data; | ||||||
|     if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) { |     if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) { | ||||||
|       // Use native_exif to selectively remove GPS data |  | ||||||
|       Exif.fromPath(data.path) |       Exif.fromPath(data.path) | ||||||
|           .then((exif) { |           .then((exif) async { | ||||||
|             // Remove GPS-related attributes |             final gpsAttributes = { | ||||||
|             final gpsAttributes = [ |               'GPSLatitude': '', | ||||||
|               'GPSLatitude', |               'GPSLatitudeRef': '', | ||||||
|               'GPSLatitudeRef', |               'GPSLongitude': '', | ||||||
|               'GPSLongitude', |               'GPSLongitudeRef': '', | ||||||
|               'GPSLongitudeRef', |               'GPSAltitude': '', | ||||||
|               'GPSAltitude', |               'GPSAltitudeRef': '', | ||||||
|               'GPSAltitudeRef', |               'GPSTimeStamp': '', | ||||||
|               'GPSTimeStamp', |               'GPSProcessingMethod': '', | ||||||
|               'GPSProcessingMethod', |               'GPSDateStamp': '', | ||||||
|               'GPSDateStamp', |             }; | ||||||
|             ]; |             await exif.writeAttributes(gpsAttributes); | ||||||
|  |  | ||||||
|             // Create a map of attributes to clear |  | ||||||
|             final clearAttributes = <String, String>{}; |  | ||||||
|             for (final attr in gpsAttributes) { |  | ||||||
|               clearAttributes[attr] = ''; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Write empty values to remove GPS data |  | ||||||
|             return exif.writeAttributes(clearAttributes); |  | ||||||
|           }) |           }) | ||||||
|           .then((_) { |           .then( | ||||||
|             // Continue with upload after GPS data is removed |             (_) => _processUpload( | ||||||
|             _processUpload( |  | ||||||
|               fileData, |               fileData, | ||||||
|               atk, |               atk, | ||||||
|               baseUrl, |               baseUrl, | ||||||
|  |               poolId, | ||||||
|               filename, |               filename, | ||||||
|               mimetype, |               mimetype, | ||||||
|               onProgress, |               onProgress, | ||||||
|               completer, |               completer, | ||||||
|             ); |             ), | ||||||
|           }) |           ) | ||||||
|           .catchError((e) { |           .catchError((e) { | ||||||
|             // If there's an error, continue with the original file |  | ||||||
|             debugPrint('Error removing GPS EXIF data: $e'); |             debugPrint('Error removing GPS EXIF data: $e'); | ||||||
|             _processUpload( |             return _processUpload( | ||||||
|               fileData, |               fileData, | ||||||
|               atk, |               atk, | ||||||
|               baseUrl, |               baseUrl, | ||||||
|  |               poolId, | ||||||
|               filename, |               filename, | ||||||
|               mimetype, |               mimetype, | ||||||
|               onProgress, |               onProgress, | ||||||
| @@ -109,11 +110,11 @@ Completer<SnCloudFile?> putMediaToCloud({ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // If not an image or on web, continue with normal upload |  | ||||||
|   _processUpload( |   _processUpload( | ||||||
|     fileData, |     fileData, | ||||||
|     atk, |     atk, | ||||||
|     baseUrl, |     baseUrl, | ||||||
|  |     poolId, | ||||||
|     filename, |     filename, | ||||||
|     mimetype, |     mimetype, | ||||||
|     onProgress, |     onProgress, | ||||||
| @@ -127,6 +128,7 @@ Completer<SnCloudFile?> _processUpload( | |||||||
|   UniversalFile fileData, |   UniversalFile fileData, | ||||||
|   String atk, |   String atk, | ||||||
|   String baseUrl, |   String baseUrl, | ||||||
|  |   String? poolId, | ||||||
|   String? filename, |   String? filename, | ||||||
|   String? mimetype, |   String? mimetype, | ||||||
|   Function(double progress, Duration estimate)? onProgress, |   Function(double progress, Duration estimate)? onProgress, | ||||||
| @@ -168,26 +170,81 @@ Completer<SnCloudFile?> _processUpload( | |||||||
|     return completer; |     return completer; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final Map<String, String> metadata = { |   // Create Dio instance | ||||||
|     'filename': actualFilename, |   final dio = Dio( | ||||||
|     'content-type': actualMimetype, |     BaseOptions( | ||||||
|   }; |       baseUrl: baseUrl, | ||||||
|  |       headers: { | ||||||
|  |         'Authorization': 'AtField $atk', | ||||||
|  |         'Accept': 'application/json', | ||||||
|  |         'Content-Type': 'application/json', | ||||||
|  |       }, | ||||||
|  |     ), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   final client = TusClient(file); |   final uploader = FileUploader(dio); | ||||||
|   client |  | ||||||
|       .upload( |   // Get File object | ||||||
|         uri: Uri.parse('$baseUrl/drive/tus'), |   File fileObj; | ||||||
|         headers: {'Authorization': 'AtField $atk'}, |   if (file.path.isNotEmpty) { | ||||||
|         metadata: metadata, |     fileObj = File(file.path); | ||||||
|         onComplete: (lastResponse) { |     // Call progress start | ||||||
|           final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!); |     onProgress?.call(0.0, Duration.zero); | ||||||
|           completer.complete(SnCloudFile.fromJson(resp)); |     uploader | ||||||
|         }, |         .uploadFile( | ||||||
|         onProgress: (double progress, Duration estimate) { |           file: fileObj, | ||||||
|           onProgress?.call(progress, estimate); |           fileName: actualFilename, | ||||||
|         }, |           contentType: actualMimetype, | ||||||
|  |           poolId: poolId, | ||||||
|         ) |         ) | ||||||
|       .catchError(completer.completeError); |         .then((result) { | ||||||
|  |           // Call progress end | ||||||
|  |           onProgress?.call(1.0, Duration.zero); | ||||||
|  |           completer.complete(result); | ||||||
|  |         }) | ||||||
|  |         .catchError((e) { | ||||||
|  |           completer.completeError(e); | ||||||
|  |           throw e; | ||||||
|  |         }); | ||||||
|  |   } else { | ||||||
|  |     // Write to temp file | ||||||
|  |     getTemporaryDirectory() | ||||||
|  |         .then((tempDir) { | ||||||
|  |           final tempFile = File('${tempDir.path}/temp_upload_$actualFilename'); | ||||||
|  |           file | ||||||
|  |               .readAsBytes() | ||||||
|  |               .then((bytes) => tempFile.writeAsBytes(bytes)) | ||||||
|  |               .then((_) { | ||||||
|  |                 fileObj = tempFile; | ||||||
|  |                 // Call progress start | ||||||
|  |                 onProgress?.call(0.0, Duration.zero); | ||||||
|  |                 uploader | ||||||
|  |                     .uploadFile( | ||||||
|  |                       file: fileObj, | ||||||
|  |                       fileName: actualFilename, | ||||||
|  |                       contentType: actualMimetype, | ||||||
|  |                       poolId: poolId, | ||||||
|  |                     ) | ||||||
|  |                     .then((result) { | ||||||
|  |                       // Call progress end | ||||||
|  |                       onProgress?.call(1.0, Duration.zero); | ||||||
|  |                       completer.complete(result); | ||||||
|  |                     }) | ||||||
|  |                     .catchError((e) { | ||||||
|  |                       completer.completeError(e); | ||||||
|  |                       throw e; | ||||||
|  |                     }); | ||||||
|  |               }) | ||||||
|  |               .catchError((e) { | ||||||
|  |                 completer.completeError(e); | ||||||
|  |                 throw e; | ||||||
|  |               }); | ||||||
|  |         }) | ||||||
|  |         .catchError((e) { | ||||||
|  |           completer.completeError(e); | ||||||
|  |           throw e; | ||||||
|  |         }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return completer; |   return completer; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										155
									
								
								lib/services/file_uploader.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								lib/services/file_uploader.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'dart:io'; | ||||||
|  | import 'dart:typed_data'; | ||||||
|  |  | ||||||
|  | import 'package:crypto/crypto.dart'; | ||||||
|  | import 'package:dio/dio.dart'; | ||||||
|  | import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||
|  | import 'package:island/models/file.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  |  | ||||||
|  | class FileUploader { | ||||||
|  |   final Dio _dio; | ||||||
|  |  | ||||||
|  |   FileUploader(this._dio); | ||||||
|  |  | ||||||
|  |   /// Calculates the MD5 hash of a file. | ||||||
|  |   Future<String> _calculateFileHash(File file) async { | ||||||
|  |     final bytes = await file.readAsBytes(); | ||||||
|  |     final digest = md5.convert(bytes); | ||||||
|  |     return digest.toString(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Creates an upload task for the given file. | ||||||
|  |   Future<Map<String, dynamic>> createUploadTask({ | ||||||
|  |     required File file, | ||||||
|  |     required String fileName, | ||||||
|  |     required String contentType, | ||||||
|  |     String? poolId, | ||||||
|  |     String? bundleId, | ||||||
|  |     String? encryptPassword, | ||||||
|  |     String? expiredAt, | ||||||
|  |     int? chunkSize, | ||||||
|  |   }) async { | ||||||
|  |     final hash = await _calculateFileHash(file); | ||||||
|  |     final fileSize = await file.length(); | ||||||
|  |  | ||||||
|  |     final response = await _dio.post( | ||||||
|  |       '/drive/files/upload/create', | ||||||
|  |       data: { | ||||||
|  |         'hash': hash, | ||||||
|  |         'file_name': fileName, | ||||||
|  |         'file_size': fileSize, | ||||||
|  |         'content_type': contentType, | ||||||
|  |         'pool_id': poolId, | ||||||
|  |         'bundle_id': bundleId, | ||||||
|  |         'encrypt_password': encryptPassword, | ||||||
|  |         'expired_at': expiredAt, | ||||||
|  |         'chunk_size': chunkSize, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return response.data; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Uploads a single chunk of the file. | ||||||
|  |   Future<void> uploadChunk({ | ||||||
|  |     required String taskId, | ||||||
|  |     required int chunkIndex, | ||||||
|  |     required Uint8List chunkData, | ||||||
|  |   }) async { | ||||||
|  |     final formData = FormData.fromMap({ | ||||||
|  |       'chunk': MultipartFile.fromBytes( | ||||||
|  |         chunkData, | ||||||
|  |         filename: 'chunk_$chunkIndex', | ||||||
|  |       ), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await _dio.post( | ||||||
|  |       '/drive/files/upload/chunk/$taskId/$chunkIndex', | ||||||
|  |       data: formData, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Completes the upload and returns the CloudFile object. | ||||||
|  |   Future<SnCloudFile> completeUpload(String taskId) async { | ||||||
|  |     final response = await _dio.post('/drive/files/upload/complete/$taskId'); | ||||||
|  |  | ||||||
|  |     return SnCloudFile.fromJson(response.data); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Uploads a file in chunks using the multi-part API. | ||||||
|  |   Future<SnCloudFile> uploadFile({ | ||||||
|  |     required File file, | ||||||
|  |     required String fileName, | ||||||
|  |     required String contentType, | ||||||
|  |     String? poolId, | ||||||
|  |     String? bundleId, | ||||||
|  |     String? encryptPassword, | ||||||
|  |     String? expiredAt, | ||||||
|  |     int? customChunkSize, | ||||||
|  |   }) async { | ||||||
|  |     // Step 1: Create upload task | ||||||
|  |     final createResponse = await createUploadTask( | ||||||
|  |       file: file, | ||||||
|  |       fileName: fileName, | ||||||
|  |       contentType: contentType, | ||||||
|  |       poolId: poolId, | ||||||
|  |       bundleId: bundleId, | ||||||
|  |       encryptPassword: encryptPassword, | ||||||
|  |       expiredAt: expiredAt, | ||||||
|  |       chunkSize: customChunkSize, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (createResponse['file_exists'] == true) { | ||||||
|  |       // File already exists, return the existing file | ||||||
|  |       return SnCloudFile.fromJson(createResponse['file']); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final taskId = createResponse['task_id'] as String; | ||||||
|  |     final chunkSize = createResponse['chunk_size'] as int; | ||||||
|  |     final chunksCount = createResponse['chunks_count'] as int; | ||||||
|  |  | ||||||
|  |     // Step 2: Upload chunks | ||||||
|  |     final stream = file.openRead(); | ||||||
|  |     final chunks = <Uint8List>[]; | ||||||
|  |     int bytesRead = 0; | ||||||
|  |     final buffer = BytesBuilder(); | ||||||
|  |  | ||||||
|  |     await for (final chunk in stream) { | ||||||
|  |       buffer.add(chunk); | ||||||
|  |       bytesRead += chunk.length; | ||||||
|  |  | ||||||
|  |       if (bytesRead >= chunkSize) { | ||||||
|  |         chunks.add(buffer.takeBytes()); | ||||||
|  |         bytesRead = 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Add remaining bytes as last chunk | ||||||
|  |     if (buffer.length > 0) { | ||||||
|  |       chunks.add(buffer.takeBytes()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Ensure we have the correct number of chunks | ||||||
|  |     if (chunks.length != chunksCount) { | ||||||
|  |       throw Exception( | ||||||
|  |         'Chunk count mismatch: expected $chunksCount, got ${chunks.length}', | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Upload each chunk | ||||||
|  |     for (int i = 0; i < chunks.length; i++) { | ||||||
|  |       await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Step 3: Complete upload | ||||||
|  |     return await completeUpload(taskId); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Riverpod provider for the FileUploader service | ||||||
|  | final fileUploaderProvider = Provider<FileUploader>((ref) { | ||||||
|  |   final dio = ref.watch(apiClientProvider); | ||||||
|  |   return FileUploader(dio); | ||||||
|  | }); | ||||||
| @@ -1,230 +1,47 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:developer'; |  | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:dio/dio.dart'; | import 'package:dio/dio.dart'; | ||||||
| import 'package:firebase_messaging/firebase_messaging.dart'; |  | ||||||
| import 'package:flutter/foundation.dart'; |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_local_notifications/flutter_local_notifications.dart'; |  | ||||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||
| import 'package:go_router/go_router.dart'; |  | ||||||
| import 'package:island/main.dart'; |  | ||||||
| import 'package:island/route.dart'; |  | ||||||
| import 'package:island/models/account.dart'; |  | ||||||
| import 'package:island/pods/websocket.dart'; |  | ||||||
| import 'package:island/widgets/app_notification.dart'; |  | ||||||
| import 'package:top_snackbar_flutter/top_snack_bar.dart'; |  | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; |  | ||||||
|  |  | ||||||
| final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = | // Conditional imports based on platform | ||||||
|     FlutterLocalNotificationsPlugin(); | import 'notify.windows.dart' as windows_notify; | ||||||
|  | import 'notify.universal.dart' as universal_notify; | ||||||
| AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; |  | ||||||
|  |  | ||||||
| void _onAppLifecycleChanged(AppLifecycleState state) { |  | ||||||
|   _appLifecycleState = state; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | // Platform-specific delegation | ||||||
| Future<void> initializeLocalNotifications() async { | Future<void> initializeLocalNotifications() async { | ||||||
|   const AndroidInitializationSettings initializationSettingsAndroid = |   if (Platform.isWindows) { | ||||||
|       AndroidInitializationSettings('@mipmap/ic_launcher'); |     return windows_notify.initializeLocalNotifications(); | ||||||
|  |  | ||||||
|   const DarwinInitializationSettings initializationSettingsIOS = |  | ||||||
|       DarwinInitializationSettings(); |  | ||||||
|  |  | ||||||
|   const DarwinInitializationSettings initializationSettingsMacOS = |  | ||||||
|       DarwinInitializationSettings(); |  | ||||||
|  |  | ||||||
|   const LinuxInitializationSettings initializationSettingsLinux = |  | ||||||
|       LinuxInitializationSettings(defaultActionName: 'Open notification'); |  | ||||||
|  |  | ||||||
|   const WindowsInitializationSettings initializationSettingsWindows = |  | ||||||
|       WindowsInitializationSettings( |  | ||||||
|         appName: 'Island', |  | ||||||
|         appUserModelId: 'dev.solsynth.solian', |  | ||||||
|         guid: 'dev.solsynth.solian', |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|   const InitializationSettings initializationSettings = InitializationSettings( |  | ||||||
|     android: initializationSettingsAndroid, |  | ||||||
|     iOS: initializationSettingsIOS, |  | ||||||
|     macOS: initializationSettingsMacOS, |  | ||||||
|     linux: initializationSettingsLinux, |  | ||||||
|     windows: initializationSettingsWindows, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   await flutterLocalNotificationsPlugin.initialize( |  | ||||||
|     initializationSettings, |  | ||||||
|     onDidReceiveNotificationResponse: (NotificationResponse response) async { |  | ||||||
|       final payload = response.payload; |  | ||||||
|       if (payload != null) { |  | ||||||
|         if (payload.startsWith('/')) { |  | ||||||
|           // In-app routes |  | ||||||
|           rootNavigatorKey.currentContext?.push(payload); |  | ||||||
|   } else { |   } else { | ||||||
|           // External URLs |     return universal_notify.initializeLocalNotifications(); | ||||||
|           launchUrlString(payload); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   WidgetsBinding.instance.addObserver( |  | ||||||
|     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class LifecycleEventHandler extends WidgetsBindingObserver { |  | ||||||
|   final void Function(AppLifecycleState) onAppLifecycleChanged; |  | ||||||
|  |  | ||||||
|   LifecycleEventHandler({required this.onAppLifecycleChanged}); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { |  | ||||||
|     onAppLifecycleChanged(state); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| StreamSubscription<WebSocketPacket> setupNotificationListener( | StreamSubscription setupNotificationListener( | ||||||
|   BuildContext context, |   BuildContext context, | ||||||
|   WidgetRef ref, |   WidgetRef ref, | ||||||
| ) { | ) { | ||||||
|   final ws = ref.watch(websocketProvider); |   if (Platform.isWindows) { | ||||||
|   return ws.dataStream.listen((pkt) async { |     return windows_notify.setupNotificationListener(context, ref); | ||||||
|     if (pkt.type == "notifications.new") { |  | ||||||
|       final notification = SnNotification.fromJson(pkt.data!); |  | ||||||
|       if (_appLifecycleState == AppLifecycleState.resumed) { |  | ||||||
|         // App is focused, show in-app notification |  | ||||||
|         log( |  | ||||||
|           '[Notification] Showing in-app notification: ${notification.title}', |  | ||||||
|         ); |  | ||||||
|         showTopSnackBar( |  | ||||||
|           globalOverlay.currentState!, |  | ||||||
|           Center( |  | ||||||
|             child: ConstrainedBox( |  | ||||||
|               constraints: const BoxConstraints(maxWidth: 480), |  | ||||||
|               child: NotificationCard(notification: notification), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           onTap: () { |  | ||||||
|             if (notification.meta['action_uri'] != null) { |  | ||||||
|               var uri = notification.meta['action_uri'] as String; |  | ||||||
|               if (uri.startsWith('/')) { |  | ||||||
|                 // In-app routes |  | ||||||
|                 rootNavigatorKey.currentContext?.push( |  | ||||||
|                   notification.meta['action_uri'], |  | ||||||
|                 ); |  | ||||||
|   } else { |   } else { | ||||||
|                 // External URLs |     return universal_notify.setupNotificationListener(context, ref); | ||||||
|                 launchUrlString(uri); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|           }, |  | ||||||
|           onDismissed: () {}, |  | ||||||
|           dismissType: DismissType.onSwipe, |  | ||||||
|           displayDuration: const Duration(seconds: 5), |  | ||||||
|           snackBarPosition: SnackBarPosition.top, |  | ||||||
|           padding: EdgeInsets.only( |  | ||||||
|             left: 16, |  | ||||||
|             right: 16, |  | ||||||
|             top: |  | ||||||
|                 (!kIsWeb && |  | ||||||
|                         (Platform.isMacOS || |  | ||||||
|                             Platform.isWindows || |  | ||||||
|                             Platform.isLinux)) |  | ||||||
|                     ? 28 |  | ||||||
|                     // ignore: use_build_context_synchronously |  | ||||||
|                     : MediaQuery.of(context).padding.top + 16, |  | ||||||
|             bottom: 16, |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
|       } else { |  | ||||||
|         // App is in background, show system notification (only on supported platforms) |  | ||||||
|         if (!kIsWeb && !Platform.isIOS) { |  | ||||||
|           log( |  | ||||||
|             '[Notification] Showing system notification: ${notification.title}', |  | ||||||
|           ); |  | ||||||
|           const AndroidNotificationDetails androidNotificationDetails = |  | ||||||
|               AndroidNotificationDetails( |  | ||||||
|                 'channel_id', |  | ||||||
|                 'channel_name', |  | ||||||
|                 channelDescription: 'channel_description', |  | ||||||
|                 importance: Importance.max, |  | ||||||
|                 priority: Priority.high, |  | ||||||
|                 ticker: 'ticker', |  | ||||||
|               ); |  | ||||||
|           const NotificationDetails notificationDetails = NotificationDetails( |  | ||||||
|             android: androidNotificationDetails, |  | ||||||
|           ); |  | ||||||
|           await flutterLocalNotificationsPlugin.show( |  | ||||||
|             0, |  | ||||||
|             notification.title, |  | ||||||
|             notification.content, |  | ||||||
|             notificationDetails, |  | ||||||
|             payload: notification.meta['action_uri'] as String?, |  | ||||||
|           ); |  | ||||||
|         } else { |  | ||||||
|           log( |  | ||||||
|             '[Notification] Skipping system notification for unsupported platform: ${notification.title}', |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| Future<void> subscribePushNotification( | Future<void> subscribePushNotification( | ||||||
|   Dio apiClient, { |   Dio apiClient, { | ||||||
|   bool detailedErrors = false, |   bool detailedErrors = false, | ||||||
| }) async { | }) async { | ||||||
|   if (!kIsWeb && Platform.isLinux) { |   if (Platform.isWindows) { | ||||||
|     return; |     return windows_notify.subscribePushNotification( | ||||||
|   } |  | ||||||
|   await FirebaseMessaging.instance.requestPermission( |  | ||||||
|     alert: true, |  | ||||||
|     badge: true, |  | ||||||
|     sound: true, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   String? deviceToken; |  | ||||||
|   if (kIsWeb) { |  | ||||||
|     deviceToken = await FirebaseMessaging.instance.getToken( |  | ||||||
|       vapidKey: |  | ||||||
|           "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", |  | ||||||
|     ); |  | ||||||
|   } else if (Platform.isAndroid) { |  | ||||||
|     deviceToken = await FirebaseMessaging.instance.getToken(); |  | ||||||
|   } else if (Platform.isIOS) { |  | ||||||
|     deviceToken = await FirebaseMessaging.instance.getAPNSToken(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   FirebaseMessaging.instance.onTokenRefresh |  | ||||||
|       .listen((fcmToken) { |  | ||||||
|         _putTokenToRemote(apiClient, fcmToken, 1); |  | ||||||
|       }) |  | ||||||
|       .onError((err) { |  | ||||||
|         log("Failed to get firebase cloud messaging push token: $err"); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|   if (deviceToken != null) { |  | ||||||
|     _putTokenToRemote( |  | ||||||
|       apiClient, |       apiClient, | ||||||
|       deviceToken, |       detailedErrors: detailedErrors, | ||||||
|       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, |  | ||||||
|     ); |     ); | ||||||
|   } else if (detailedErrors) { |   } else { | ||||||
|     throw Exception("Failed to get device token for push notifications."); |     return universal_notify.subscribePushNotification( | ||||||
|   } |       apiClient, | ||||||
| } |       detailedErrors: detailedErrors, | ||||||
|  |  | ||||||
| Future<void> _putTokenToRemote( |  | ||||||
|   Dio apiClient, |  | ||||||
|   String token, |  | ||||||
|   int provider, |  | ||||||
| ) async { |  | ||||||
|   await apiClient.put( |  | ||||||
|     "/pusher/notifications/subscription", |  | ||||||
|     data: {"provider": provider, "device_token": token}, |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										232
									
								
								lib/services/notify.universal.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								lib/services/notify.universal.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'dart:developer'; | ||||||
|  | import 'dart:io'; | ||||||
|  |  | ||||||
|  | import 'package:dio/dio.dart'; | ||||||
|  | import 'package:firebase_messaging/firebase_messaging.dart'; | ||||||
|  | import 'package:flutter/foundation.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||||
|  | import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||
|  | import 'package:go_router/go_router.dart'; | ||||||
|  | import 'package:island/main.dart'; | ||||||
|  | import 'package:island/route.dart'; | ||||||
|  | import 'package:island/models/account.dart'; | ||||||
|  | import 'package:island/pods/websocket.dart'; | ||||||
|  | import 'package:island/widgets/app_notification.dart'; | ||||||
|  | import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||||
|  | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
|  |  | ||||||
|  | final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = | ||||||
|  |     FlutterLocalNotificationsPlugin(); | ||||||
|  |  | ||||||
|  | AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; | ||||||
|  |  | ||||||
|  | void _onAppLifecycleChanged(AppLifecycleState state) { | ||||||
|  |   _appLifecycleState = state; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<void> initializeLocalNotifications() async { | ||||||
|  |   const AndroidInitializationSettings initializationSettingsAndroid = | ||||||
|  |       AndroidInitializationSettings('@mipmap/ic_launcher'); | ||||||
|  |  | ||||||
|  |   const DarwinInitializationSettings initializationSettingsIOS = | ||||||
|  |       DarwinInitializationSettings(); | ||||||
|  |  | ||||||
|  |   const DarwinInitializationSettings initializationSettingsMacOS = | ||||||
|  |       DarwinInitializationSettings(); | ||||||
|  |  | ||||||
|  |   const LinuxInitializationSettings initializationSettingsLinux = | ||||||
|  |       LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||||
|  |  | ||||||
|  |   const WindowsInitializationSettings initializationSettingsWindows = | ||||||
|  |       WindowsInitializationSettings( | ||||||
|  |         appName: 'Island', | ||||||
|  |         appUserModelId: 'dev.solsynth.solian', | ||||||
|  |         guid: 'dev.solsynth.solian', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |   const InitializationSettings initializationSettings = InitializationSettings( | ||||||
|  |     android: initializationSettingsAndroid, | ||||||
|  |     iOS: initializationSettingsIOS, | ||||||
|  |     macOS: initializationSettingsMacOS, | ||||||
|  |     linux: initializationSettingsLinux, | ||||||
|  |     windows: initializationSettingsWindows, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   await flutterLocalNotificationsPlugin.initialize( | ||||||
|  |     initializationSettings, | ||||||
|  |     onDidReceiveNotificationResponse: (NotificationResponse response) async { | ||||||
|  |       final payload = response.payload; | ||||||
|  |       if (payload != null) { | ||||||
|  |         if (payload.startsWith('/')) { | ||||||
|  |           // In-app routes | ||||||
|  |           rootNavigatorKey.currentContext?.push(payload); | ||||||
|  |         } else { | ||||||
|  |           // External URLs | ||||||
|  |           launchUrlString(payload); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   WidgetsBinding.instance.addObserver( | ||||||
|  |     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class LifecycleEventHandler extends WidgetsBindingObserver { | ||||||
|  |   final void Function(AppLifecycleState) onAppLifecycleChanged; | ||||||
|  |  | ||||||
|  |   LifecycleEventHandler({required this.onAppLifecycleChanged}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||||
|  |     onAppLifecycleChanged(state); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||||
|  |   BuildContext context, | ||||||
|  |   WidgetRef ref, | ||||||
|  | ) { | ||||||
|  |   final ws = ref.watch(websocketProvider); | ||||||
|  |   return ws.dataStream.listen((pkt) async { | ||||||
|  |     if (pkt.type == "notifications.new") { | ||||||
|  |       final notification = SnNotification.fromJson(pkt.data!); | ||||||
|  |       if (_appLifecycleState == AppLifecycleState.resumed) { | ||||||
|  |         // App is focused, show in-app notification | ||||||
|  |         log( | ||||||
|  |           '[Notification] Showing in-app notification: ${notification.title}', | ||||||
|  |         ); | ||||||
|  |         showTopSnackBar( | ||||||
|  |           globalOverlay.currentState!, | ||||||
|  |           Center( | ||||||
|  |             child: ConstrainedBox( | ||||||
|  |               constraints: const BoxConstraints(maxWidth: 480), | ||||||
|  |               child: NotificationCard(notification: notification), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           onTap: () { | ||||||
|  |             if (notification.meta['action_uri'] != null) { | ||||||
|  |               var uri = notification.meta['action_uri'] as String; | ||||||
|  |               if (uri.startsWith('/')) { | ||||||
|  |                 // In-app routes | ||||||
|  |                 rootNavigatorKey.currentContext?.push( | ||||||
|  |                   notification.meta['action_uri'], | ||||||
|  |                 ); | ||||||
|  |               } else { | ||||||
|  |                 // External URLs | ||||||
|  |                 launchUrlString(uri); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onDismissed: () {}, | ||||||
|  |           dismissType: DismissType.onSwipe, | ||||||
|  |           displayDuration: const Duration(seconds: 5), | ||||||
|  |           snackBarPosition: SnackBarPosition.top, | ||||||
|  |           padding: EdgeInsets.only( | ||||||
|  |             left: 16, | ||||||
|  |             right: 16, | ||||||
|  |             top: | ||||||
|  |                 (!kIsWeb && | ||||||
|  |                         (Platform.isMacOS || | ||||||
|  |                             Platform.isWindows || | ||||||
|  |                             Platform.isLinux)) | ||||||
|  |                     ? 28 | ||||||
|  |                     // ignore: use_build_context_synchronously | ||||||
|  |                     : MediaQuery.of(context).padding.top + 16, | ||||||
|  |             bottom: 16, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         // App is in background, show system notification (only on supported platforms) | ||||||
|  |         if (!kIsWeb && !Platform.isIOS) { | ||||||
|  |           log( | ||||||
|  |             '[Notification] Showing system notification: ${notification.title}', | ||||||
|  |           ); | ||||||
|  |  | ||||||
|  |           // Use flutter_local_notifications for universal platforms | ||||||
|  |           const AndroidNotificationDetails androidNotificationDetails = | ||||||
|  |               AndroidNotificationDetails( | ||||||
|  |                 'channel_id', | ||||||
|  |                 'channel_name', | ||||||
|  |                 channelDescription: 'channel_description', | ||||||
|  |                 importance: Importance.max, | ||||||
|  |                 priority: Priority.high, | ||||||
|  |                 ticker: 'ticker', | ||||||
|  |               ); | ||||||
|  |           const NotificationDetails notificationDetails = NotificationDetails( | ||||||
|  |             android: androidNotificationDetails, | ||||||
|  |           ); | ||||||
|  |           await flutterLocalNotificationsPlugin.show( | ||||||
|  |             0, | ||||||
|  |             notification.title, | ||||||
|  |             notification.content, | ||||||
|  |             notificationDetails, | ||||||
|  |             payload: notification.meta['action_uri'] as String?, | ||||||
|  |           ); | ||||||
|  |         } else { | ||||||
|  |           log( | ||||||
|  |             '[Notification] Skipping system notification for unsupported platform: ${notification.title}', | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<void> subscribePushNotification( | ||||||
|  |   Dio apiClient, { | ||||||
|  |   bool detailedErrors = false, | ||||||
|  | }) async { | ||||||
|  |   if (!kIsWeb && Platform.isLinux) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   await FirebaseMessaging.instance.requestPermission( | ||||||
|  |     alert: true, | ||||||
|  |     badge: true, | ||||||
|  |     sound: true, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   String? deviceToken; | ||||||
|  |   if (kIsWeb) { | ||||||
|  |     deviceToken = await FirebaseMessaging.instance.getToken( | ||||||
|  |       vapidKey: | ||||||
|  |           "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", | ||||||
|  |     ); | ||||||
|  |   } else if (Platform.isAndroid) { | ||||||
|  |     deviceToken = await FirebaseMessaging.instance.getToken(); | ||||||
|  |   } else if (Platform.isIOS) { | ||||||
|  |     deviceToken = await FirebaseMessaging.instance.getAPNSToken(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   FirebaseMessaging.instance.onTokenRefresh | ||||||
|  |       .listen((fcmToken) { | ||||||
|  |         _putTokenToRemote(apiClient, fcmToken, 1); | ||||||
|  |       }) | ||||||
|  |       .onError((err) { | ||||||
|  |         log("Failed to get firebase cloud messaging push token: $err"); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |   if (deviceToken != null) { | ||||||
|  |     _putTokenToRemote( | ||||||
|  |       apiClient, | ||||||
|  |       deviceToken, | ||||||
|  |       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, | ||||||
|  |     ); | ||||||
|  |   } else if (detailedErrors) { | ||||||
|  |     throw Exception("Failed to get device token for push notifications."); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<void> _putTokenToRemote( | ||||||
|  |   Dio apiClient, | ||||||
|  |   String token, | ||||||
|  |   int provider, | ||||||
|  | ) async { | ||||||
|  |   await apiClient.put( | ||||||
|  |     "/ring/notifications/subscription", | ||||||
|  |     data: {"provider": provider, "device_token": token}, | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										176
									
								
								lib/services/notify.windows.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								lib/services/notify.windows.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'dart:developer'; | ||||||
|  | import 'dart:io'; | ||||||
|  |  | ||||||
|  | import 'package:firebase_messaging/firebase_messaging.dart'; | ||||||
|  | import 'package:flutter/foundation.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||
|  | import 'package:go_router/go_router.dart'; | ||||||
|  | import 'package:island/main.dart'; | ||||||
|  | import 'package:island/route.dart'; | ||||||
|  | import 'package:island/models/account.dart'; | ||||||
|  | import 'package:island/pods/websocket.dart'; | ||||||
|  | import 'package:island/widgets/app_notification.dart'; | ||||||
|  | import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||||
|  | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
|  | import 'package:windows_notification/windows_notification.dart' | ||||||
|  |     as windows_notification; | ||||||
|  | import 'package:windows_notification/notification_message.dart'; | ||||||
|  |  | ||||||
|  | import 'package:dio/dio.dart'; | ||||||
|  |  | ||||||
|  | // Windows notification instance | ||||||
|  | windows_notification.WindowsNotification? windowsNotification; | ||||||
|  |  | ||||||
|  | AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; | ||||||
|  |  | ||||||
|  | void _onAppLifecycleChanged(AppLifecycleState state) { | ||||||
|  |   _appLifecycleState = state; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<void> initializeLocalNotifications() async { | ||||||
|  |   // Initialize Windows notification for Windows platform | ||||||
|  |   windowsNotification = windows_notification.WindowsNotification( | ||||||
|  |     applicationId: 'dev.solsynth.solian', | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   WidgetsBinding.instance.addObserver( | ||||||
|  |     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class LifecycleEventHandler extends WidgetsBindingObserver { | ||||||
|  |   final void Function(AppLifecycleState) onAppLifecycleChanged; | ||||||
|  |  | ||||||
|  |   LifecycleEventHandler({required this.onAppLifecycleChanged}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||||
|  |     onAppLifecycleChanged(state); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||||
|  |   BuildContext context, | ||||||
|  |   WidgetRef ref, | ||||||
|  | ) { | ||||||
|  |   final ws = ref.watch(websocketProvider); | ||||||
|  |   return ws.dataStream.listen((pkt) async { | ||||||
|  |     if (pkt.type == "notifications.new") { | ||||||
|  |       final notification = SnNotification.fromJson(pkt.data!); | ||||||
|  |       if (_appLifecycleState == AppLifecycleState.resumed) { | ||||||
|  |         // App is focused, show in-app notification | ||||||
|  |         log( | ||||||
|  |           '[Notification] Showing in-app notification: ${notification.title}', | ||||||
|  |         ); | ||||||
|  |         showTopSnackBar( | ||||||
|  |           globalOverlay.currentState!, | ||||||
|  |           Center( | ||||||
|  |             child: ConstrainedBox( | ||||||
|  |               constraints: const BoxConstraints(maxWidth: 480), | ||||||
|  |               child: NotificationCard(notification: notification), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           onTap: () { | ||||||
|  |             if (notification.meta['action_uri'] != null) { | ||||||
|  |               var uri = notification.meta['action_uri'] as String; | ||||||
|  |               if (uri.startsWith('/')) { | ||||||
|  |                 // In-app routes | ||||||
|  |                 rootNavigatorKey.currentContext?.push( | ||||||
|  |                   notification.meta['action_uri'], | ||||||
|  |                 ); | ||||||
|  |               } else { | ||||||
|  |                 // External URLs | ||||||
|  |                 launchUrlString(uri); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onDismissed: () {}, | ||||||
|  |           dismissType: DismissType.onSwipe, | ||||||
|  |           displayDuration: const Duration(seconds: 5), | ||||||
|  |           snackBarPosition: SnackBarPosition.top, | ||||||
|  |           padding: EdgeInsets.only( | ||||||
|  |             left: 16, | ||||||
|  |             right: 16, | ||||||
|  |             top: 28, // Windows specific padding | ||||||
|  |             bottom: 16, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         // App is in background, show Windows system notification | ||||||
|  |         log( | ||||||
|  |           '[Notification] Showing Windows system notification: ${notification.title}', | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (windowsNotification != null) { | ||||||
|  |           // Use Windows notification for Windows platform | ||||||
|  |           final notificationMessage = NotificationMessage.fromPluginTemplate( | ||||||
|  |             DateTime.now().millisecondsSinceEpoch.toString(), // unique id | ||||||
|  |             notification.title, | ||||||
|  |             notification.content, | ||||||
|  |             launch: notification.meta['action_uri'] as String?, | ||||||
|  |           ); | ||||||
|  |           await windowsNotification!.showNotificationPluginTemplate( | ||||||
|  |             notificationMessage, | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<void> subscribePushNotification( | ||||||
|  |   Dio apiClient, { | ||||||
|  |   bool detailedErrors = false, | ||||||
|  | }) async { | ||||||
|  |   if (!kIsWeb && Platform.isLinux) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   await FirebaseMessaging.instance.requestPermission( | ||||||
|  |     alert: true, | ||||||
|  |     badge: true, | ||||||
|  |     sound: true, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   String? deviceToken; | ||||||
|  |   if (kIsWeb) { | ||||||
|  |     deviceToken = await FirebaseMessaging.instance.getToken( | ||||||
|  |       vapidKey: | ||||||
|  |           "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", | ||||||
|  |     ); | ||||||
|  |   } else if (Platform.isAndroid) { | ||||||
|  |     deviceToken = await FirebaseMessaging.instance.getToken(); | ||||||
|  |   } else if (Platform.isIOS) { | ||||||
|  |     deviceToken = await FirebaseMessaging.instance.getAPNSToken(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   FirebaseMessaging.instance.onTokenRefresh | ||||||
|  |       .listen((fcmToken) { | ||||||
|  |         _putTokenToRemote(apiClient, fcmToken, 1); | ||||||
|  |       }) | ||||||
|  |       .onError((err) { | ||||||
|  |         log("Failed to get firebase cloud messaging push token: $err"); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |   if (deviceToken != null) { | ||||||
|  |     _putTokenToRemote( | ||||||
|  |       apiClient, | ||||||
|  |       deviceToken, | ||||||
|  |       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, | ||||||
|  |     ); | ||||||
|  |   } else if (detailedErrors) { | ||||||
|  |     throw Exception("Failed to get device token for push notifications."); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<void> _putTokenToRemote( | ||||||
|  |   Dio apiClient, | ||||||
|  |   String token, | ||||||
|  |   int provider, | ||||||
|  | ) async { | ||||||
|  |   await apiClient.put( | ||||||
|  |     "/ring/notifications/subscription", | ||||||
|  |     data: {"provider": provider, "device_token": token}, | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -14,7 +14,7 @@ Future<void> initializeTzdb() async { | |||||||
| } | } | ||||||
|  |  | ||||||
| Future<String> getMachineTz() async { | Future<String> getMachineTz() async { | ||||||
|   return await FlutterTimezone.getLocalTimezone(); |   return (await FlutterTimezone.getLocalTimezone()).identifier; | ||||||
| } | } | ||||||
|  |  | ||||||
| List<String> getAvailableTz() { | List<String> getAvailableTz() { | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ Future<void> initializeTzdb() async { | |||||||
| } | } | ||||||
|  |  | ||||||
| Future<String> getMachineTz() async { | Future<String> getMachineTz() async { | ||||||
|   return await FlutterTimezone.getLocalTimezone(); |   return (await FlutterTimezone.getLocalTimezone()).identifier; | ||||||
| } | } | ||||||
|  |  | ||||||
| List<String> getAvailableTz() { | List<String> getAvailableTz() { | ||||||
|   | |||||||
| @@ -1 +1,3 @@ | |||||||
| export 'udid.native.dart' if (dart.library.html) 'udid.web.dart'; | export 'udid.native.dart' | ||||||
|  |     if (dart.library.html) 'udid.web.dart' | ||||||
|  |     if (dart.library.io) 'udid.native.dart'; | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | import 'dart:io'; | ||||||
|  | import 'package:device_info_plus/device_info_plus.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter_udid/flutter_udid.dart'; | import 'package:flutter_udid/flutter_udid.dart'; | ||||||
|  |  | ||||||
| String? _cachedUdid; | String? _cachedUdid; | ||||||
| @@ -9,3 +12,18 @@ Future<String> getUdid() async { | |||||||
|   _cachedUdid = await FlutterUdid.consistentUdid; |   _cachedUdid = await FlutterUdid.consistentUdid; | ||||||
|   return _cachedUdid!; |   return _cachedUdid!; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | Future<String> getDeviceName() async { | ||||||
|  |   DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); | ||||||
|  |   if (Platform.isAndroid) { | ||||||
|  |     AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; | ||||||
|  |     return androidInfo.device; | ||||||
|  |   } else if (Platform.isIOS) { | ||||||
|  |     IosDeviceInfo iosInfo = await deviceInfo.iosInfo; | ||||||
|  |     return iosInfo.name; | ||||||
|  |   } else if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { | ||||||
|  |     return Platform.localHostname; | ||||||
|  |   } else { | ||||||
|  |     return 'unknown'.tr(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -9,3 +9,18 @@ Future<String> getUdid() async { | |||||||
|   final hash = sha256.convert(bytes); |   final hash = sha256.convert(bytes); | ||||||
|   return hash.toString(); |   return hash.toString(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | Future<String> getDeviceName() async { | ||||||
|  |   final userAgent = window.navigator.userAgent; | ||||||
|  |   if (userAgent.contains('Chrome') && !userAgent.contains('Edg')) { | ||||||
|  |     return 'Chrome'; | ||||||
|  |   } else if (userAgent.contains('Firefox')) { | ||||||
|  |     return 'Firefox'; | ||||||
|  |   } else if (userAgent.contains('Safari') && !userAgent.contains('Chrome')) { | ||||||
|  |     return 'Safari'; | ||||||
|  |   } else if (userAgent.contains('Edg')) { | ||||||
|  |     return 'Edge'; | ||||||
|  |   } else { | ||||||
|  |     return 'Browser'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								lib/utils/activity_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								lib/utils/activity_utils.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
|  | String? getActivityTitle(String? label, Map<String, dynamic>? meta) { | ||||||
|  |   if (meta == null) return label; | ||||||
|  |   if (meta['assets']?['large_text'] is String) { | ||||||
|  |     return meta['assets']?['large_text']; | ||||||
|  |   } | ||||||
|  |   return label; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String? getActivitySubtitle(Map<String, dynamic>? meta) { | ||||||
|  |   if (meta == null) return null; | ||||||
|  |   if (meta['assets']?['small_text'] is String) { | ||||||
|  |     return meta['assets']?['small_text']; | ||||||
|  |   } | ||||||
|  |   return null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | InlineSpan getActivityFullMessage(SnAccountStatus? status) { | ||||||
|  |   if (status?.meta == null) return TextSpan(text: 'No activity details available'); | ||||||
|  |   final meta = status!.meta!; | ||||||
|  |   final List<InlineSpan> spans = []; | ||||||
|  |   if (meta.containsKey('assets') && meta['assets'] is Map) { | ||||||
|  |     final assets = meta['assets'] as Map<String, dynamic>; | ||||||
|  |     if (assets.containsKey('large_text')) { | ||||||
|  |       spans.add(TextSpan(text: assets['large_text'], style: TextStyle(fontWeight: FontWeight.bold))); | ||||||
|  |     } | ||||||
|  |     if (assets.containsKey('small_text')) { | ||||||
|  |       if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); | ||||||
|  |       spans.add(TextSpan(text: assets['small_text'])); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   String normalText = ''; | ||||||
|  |   if (meta.containsKey('details')) { | ||||||
|  |     normalText += 'Details: ${meta['details']}\n'; | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('state')) { | ||||||
|  |     normalText += 'State: ${meta['state']}\n'; | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { | ||||||
|  |     final ts = meta['timestamps'] as Map<String, dynamic>; | ||||||
|  |     if (ts.containsKey('start') && ts['start'] is int) { | ||||||
|  |       final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); | ||||||
|  |       normalText += 'Started: ${start.toLocal()}\n'; | ||||||
|  |     } | ||||||
|  |     if (ts.containsKey('end') && ts['end'] is int) { | ||||||
|  |       final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); | ||||||
|  |       normalText += 'Ends: ${end.toLocal()}\n'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('party') && meta['party'] is Map) { | ||||||
|  |     final party = meta['party'] as Map<String, dynamic>; | ||||||
|  |     if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { | ||||||
|  |       final size = party['size'] as List; | ||||||
|  |       normalText += 'Party: ${size[0]}/${size[1]}\n'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('instance')) { | ||||||
|  |     normalText += 'Instance: ${meta['instance']}\n'; | ||||||
|  |   } | ||||||
|  |   // Add other keys if present | ||||||
|  |   meta.forEach((key, value) { | ||||||
|  |     if (!['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(key)) { | ||||||
|  |       normalText += '$key: $value\n'; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   if (normalText.isNotEmpty) { | ||||||
|  |     if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); | ||||||
|  |     spans.add(TextSpan(text: normalText.trimRight())); | ||||||
|  |   } | ||||||
|  |   return TextSpan(children: spans); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Widget buildActivityDetails(SnAccountStatus? status) { | ||||||
|  |   if (status?.meta == null) return Text('No activity details available'); | ||||||
|  |   final meta = status!.meta!; | ||||||
|  |   final List<Widget> children = []; | ||||||
|  |   if (meta.containsKey('assets') && meta['assets'] is Map) { | ||||||
|  |     final assets = meta['assets'] as Map<String, dynamic>; | ||||||
|  |     if (assets.containsKey('large_text')) { | ||||||
|  |       children.add(Text(assets['large_text'])); | ||||||
|  |     } | ||||||
|  |     if (assets.containsKey('small_text')) { | ||||||
|  |       children.add(Text(assets['small_text'])); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('details')) { | ||||||
|  |     children.add(Text('Details: ${meta['details']}')); | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('state')) { | ||||||
|  |     children.add(Text('State: ${meta['state']}')); | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { | ||||||
|  |     final ts = meta['timestamps'] as Map<String, dynamic>; | ||||||
|  |     if (ts.containsKey('start') && ts['start'] is int) { | ||||||
|  |       final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); | ||||||
|  |       children.add(Text('Started: ${start.toLocal()}')); | ||||||
|  |     } | ||||||
|  |     if (ts.containsKey('end') && ts['end'] is int) { | ||||||
|  |       final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); | ||||||
|  |       children.add(Text('Ends: ${end.toLocal()}')); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('party') && meta['party'] is Map) { | ||||||
|  |     final party = meta['party'] as Map<String, dynamic>; | ||||||
|  |     if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { | ||||||
|  |       final size = party['size'] as List; | ||||||
|  |       children.add(Text('Party: ${size[0]}/${size[1]}')); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('instance')) { | ||||||
|  |     children.add(Text('Instance: ${meta['instance']}')); | ||||||
|  |   } | ||||||
|  |   // Add other keys if present | ||||||
|  |   children.addAll(meta.entries.where((e) => !['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(e.key)).map((e) => Text('${e.key}: ${e.value}'))); | ||||||
|  |   return Column( | ||||||
|  |     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |     mainAxisSize: MainAxisSize.min, | ||||||
|  |     children: children, | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -6,10 +6,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | |||||||
| import 'package:island/models/account.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
|  | import 'package:island/services/time.dart'; | ||||||
| import 'package:island/services/udid.dart'; | import 'package:island/services/udid.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; | import 'package:island/widgets/content/sheet.dart'; | ||||||
| import 'package:island/widgets/response.dart'; | import 'package:island/widgets/response.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:island/widgets/extended_refresh_indicator.dart'; | import 'package:island/widgets/extended_refresh_indicator.dart'; | ||||||
| @@ -43,32 +45,11 @@ class _DeviceListTile extends StatelessWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return ListTile( |     return ExpansionTile( | ||||||
|       isThreeLine: true, |       title: Row( | ||||||
|       contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), |         spacing: 8, | ||||||
|       leading: Icon(switch (device.platform) { |  | ||||||
|         0 => Icons.device_unknown, // Unidentified |  | ||||||
|         1 => Icons.web, // Web |  | ||||||
|         2 => Icons.phone_iphone, // iOS |  | ||||||
|         3 => Icons.phone_android, // Android |  | ||||||
|         4 => Icons.laptop_mac, // macOS |  | ||||||
|         5 => Icons.window, // Windows |  | ||||||
|         6 => Icons.computer, // Linux |  | ||||||
|         _ => Icons.device_unknown, // fallback |  | ||||||
|       }).padding(top: 4), |  | ||||||
|       subtitle: Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |  | ||||||
|         children: [ |         children: [ | ||||||
|           Text( |           Flexible(child: Text(device.deviceLabel ?? device.deviceName)), | ||||||
|             'lastActiveAt'.tr( |  | ||||||
|               args: [ |  | ||||||
|                 DateFormat().format( |  | ||||||
|                   device.challenges.first.createdAt.toLocal(), |  | ||||||
|                 ), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           Text(device.challenges.first.ipAddress), |  | ||||||
|           if (device.isCurrent) |           if (device.isCurrent) | ||||||
|             Row( |             Row( | ||||||
|               children: [ |               children: [ | ||||||
| @@ -82,10 +63,29 @@ class _DeviceListTile extends StatelessWidget { | |||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ).padding(top: 4), |             ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       title: Text(device.deviceLabel ?? device.deviceName), |       subtitle: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |         children: [ | ||||||
|  |           Text( | ||||||
|  |             'lastActiveAt'.tr( | ||||||
|  |               args: [device.challenges.first.createdAt.formatSystem()], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |       leading: Icon(switch (device.platform) { | ||||||
|  |         0 => Icons.device_unknown, // Unidentified | ||||||
|  |         1 => Icons.web, // Web | ||||||
|  |         2 => Icons.phone_iphone, // iOS | ||||||
|  |         3 => Icons.phone_android, // Android | ||||||
|  |         4 => Icons.laptop_mac, // macOS | ||||||
|  |         5 => Icons.window, // Windows | ||||||
|  |         6 => Icons.computer, // Linux | ||||||
|  |         _ => Icons.device_unknown, // fallback | ||||||
|  |       }).padding(top: 4), | ||||||
|       trailing: |       trailing: | ||||||
|           isWideScreen(context) |           isWideScreen(context) | ||||||
|               ? Row( |               ? Row( | ||||||
| @@ -105,6 +105,36 @@ class _DeviceListTile extends StatelessWidget { | |||||||
|                 ], |                 ], | ||||||
|               ) |               ) | ||||||
|               : null, |               : null, | ||||||
|  |       expandedCrossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |       children: [ | ||||||
|  |         Container( | ||||||
|  |           decoration: BoxDecoration( | ||||||
|  |             color: Theme.of(context).colorScheme.surfaceVariant, | ||||||
|  |           ), | ||||||
|  |           padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||||
|  |           child: Text('authDeviceChallenges'.tr()), | ||||||
|  |         ), | ||||||
|  |         for (final challenge in device.challenges) | ||||||
|  |           ListTile( | ||||||
|  |             minTileHeight: 48, | ||||||
|  |             title: Text(DateFormat().format(challenge.createdAt.toLocal())), | ||||||
|  |             subtitle: Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |               children: [ | ||||||
|  |                 Text(challenge.ipAddress), | ||||||
|  |                 if (challenge.location != null) | ||||||
|  |                   Row( | ||||||
|  |                     spacing: 4, | ||||||
|  |                     children: | ||||||
|  |                         [challenge.location?.city, challenge.location?.country] | ||||||
|  |                             .where((e) => e?.isNotEmpty ?? false) | ||||||
|  |                             .map((e) => Text(e!)) | ||||||
|  |                             .toList(), | ||||||
|  |                   ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -176,11 +206,38 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     return SheetScaffold( |     return SheetScaffold( | ||||||
|       titleText: 'authSessions'.tr(), |       titleText: 'authSessions'.tr(), | ||||||
|  |       child: Column( | ||||||
|  |         children: [ | ||||||
|  |           if (!wideScreen) | ||||||
|  |             Container( | ||||||
|  |               width: double.infinity, | ||||||
|  |               padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||||
|  |               color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |               child: Row( | ||||||
|  |                 mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                 spacing: 8, | ||||||
|  |                 children: [ | ||||||
|  |                   const Icon(Symbols.info, size: 16).padding(top: 2), | ||||||
|  |                   Flexible( | ||||||
|  |                     child: Text( | ||||||
|  |                       'authDeviceHint'.tr(), | ||||||
|  |                       style: TextStyle( | ||||||
|  |                         color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           Expanded( | ||||||
|             child: authDevices.when( |             child: authDevices.when( | ||||||
|               data: |               data: | ||||||
|                   (data) => ExtendedRefreshIndicator( |                   (data) => ExtendedRefreshIndicator( | ||||||
|                     onRefresh: |                     onRefresh: | ||||||
|                   () => Future.sync(() => ref.invalidate(authDevicesProvider)), |                         () => Future.sync( | ||||||
|  |                           () => ref.invalidate(authDevicesProvider), | ||||||
|  |                         ), | ||||||
|                     child: ListView.builder( |                     child: ListView.builder( | ||||||
|                       padding: EdgeInsets.zero, |                       padding: EdgeInsets.zero, | ||||||
|                       itemCount: data.length, |                       itemCount: data.length, | ||||||
| @@ -221,9 +278,24 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|                                   'authDeviceLogout'.tr(), |                                   'authDeviceLogout'.tr(), | ||||||
|                                 ); |                                 ); | ||||||
|                                 if (confirm && context.mounted) { |                                 if (confirm && context.mounted) { | ||||||
|                             logoutDevice(device.deviceId); |                                   try { | ||||||
|  |                                     showLoadingModal(context); | ||||||
|  |                                     final apiClient = ref.watch( | ||||||
|  |                                       apiClientProvider, | ||||||
|  |                                     ); | ||||||
|  |                                     await apiClient.delete( | ||||||
|  |                                       '/id/accounts/me/devices/${device.deviceId}', | ||||||
|  |                                     ); | ||||||
|  |                                     ref.invalidate(authDevicesProvider); | ||||||
|  |                                   } catch (err) { | ||||||
|  |                                     showErrorAlert(err); | ||||||
|  |                                   } finally { | ||||||
|  |                                     if (context.mounted) { | ||||||
|  |                                       hideLoadingModal(context); | ||||||
|                                     } |                                     } | ||||||
|                           return false; // Don't dismiss |                                   } | ||||||
|  |                                 } | ||||||
|  |                                 return confirm; | ||||||
|                               } |                               } | ||||||
|                             }, |                             }, | ||||||
|                             child: _DeviceListTile( |                             child: _DeviceListTile( | ||||||
| @@ -243,6 +315,9 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|                   ), |                   ), | ||||||
|               loading: () => ResponseLoadingWidget(), |               loading: () => ResponseLoadingWidget(), | ||||||
|             ), |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -147,6 +147,7 @@ class AccountProfileCard extends HookConsumerWidget { | |||||||
|                       if (data.badges.isNotEmpty) |                       if (data.badges.isNotEmpty) | ||||||
|                         BadgeList(badges: data.badges).padding(top: 12), |                         BadgeList(badges: data.badges).padding(top: 12), | ||||||
|                       LevelingProgressCard( |                       LevelingProgressCard( | ||||||
|  |                         isCompact: true, | ||||||
|                         level: data.profile.level, |                         level: data.profile.level, | ||||||
|                         experience: data.profile.experience, |                         experience: data.profile.experience, | ||||||
|                         progress: data.profile.levelingProgress, |                         progress: data.profile.levelingProgress, | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:island/models/activity.dart'; | import 'package:island/models/activity.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
|  | import 'package:island/utils/activity_utils.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| @@ -75,7 +76,10 @@ class EventDetailsWidget extends StatelessWidget { | |||||||
|                       child: Column( |                       child: Column( | ||||||
|                         crossAxisAlignment: CrossAxisAlignment.start, |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                         children: [ |                         children: [ | ||||||
|                           Text(status.label), |                           if ((getActivityTitle(status.label, status.meta) ?? status.label).isNotEmpty) | ||||||
|  |                             Text(getActivityTitle(status.label, status.meta) ?? status.label), | ||||||
|  |                           if (getActivitySubtitle(status.meta) != null) | ||||||
|  |                             Text(getActivitySubtitle(status.meta)!).fontSize(11).opacity(0.8), | ||||||
|                           Text( |                           Text( | ||||||
|                             '${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}', |                             '${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}', | ||||||
|                           ).fontSize(11).opacity(0.8), |                           ).fontSize(11).opacity(0.8), | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:google_fonts/google_fonts.dart'; |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| @@ -8,50 +7,152 @@ class LevelingProgressCard extends StatelessWidget { | |||||||
|   final int level; |   final int level; | ||||||
|   final int experience; |   final int experience; | ||||||
|   final double progress; |   final double progress; | ||||||
|  |   final VoidCallback? onTap; | ||||||
|  |   final bool isCompact; | ||||||
|  |  | ||||||
|   const LevelingProgressCard({ |   const LevelingProgressCard({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.level, |     required this.level, | ||||||
|     required this.experience, |     required this.experience, | ||||||
|     required this.progress, |     required this.progress, | ||||||
|  |     this.onTap, | ||||||
|  |     this.isCompact = false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Card( |     // Calculate level stage (1-12, each stage covers 10 levels) | ||||||
|  |     int stage = ((level - 1) ~/ 10) + 1; | ||||||
|  |     stage = stage.clamp(1, 12); // Ensure stage is within 1-12 | ||||||
|  |  | ||||||
|  |     // Define colors for each stage | ||||||
|  |     const List<Color> stageColors = [ | ||||||
|  |       Colors.green, | ||||||
|  |       Colors.blue, | ||||||
|  |       Colors.teal, | ||||||
|  |       Colors.cyan, | ||||||
|  |       Colors.indigo, | ||||||
|  |       Colors.lime, | ||||||
|  |       Colors.yellow, | ||||||
|  |       Colors.amber, | ||||||
|  |       Colors.orange, | ||||||
|  |       Colors.deepOrange, | ||||||
|  |       Colors.pink, | ||||||
|  |       Colors.red, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     Color stageColor = stageColors[stage - 1]; | ||||||
|  |  | ||||||
|  |     // Compact mode adjustments | ||||||
|  |     final double levelFontSize = isCompact ? 14 : 18; | ||||||
|  |     final double stageFontSize = isCompact ? 13 : 14; | ||||||
|  |     final double experienceFontSize = isCompact ? 12 : 14; | ||||||
|  |     final double progressHeight = isCompact ? 6 : 10; | ||||||
|  |     final double horizontalPadding = isCompact ? 16 : 20; | ||||||
|  |     final double verticalPadding = isCompact ? 12 : 16; | ||||||
|  |     final double gapSize = isCompact ? 4 : 8; | ||||||
|  |     final double rowSpacing = 12; | ||||||
|  |  | ||||||
|  |     final cardContent = Card( | ||||||
|       margin: EdgeInsets.zero, |       margin: EdgeInsets.zero, | ||||||
|  |       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), | ||||||
|  |       child: InkWell( | ||||||
|  |         onTap: onTap, | ||||||
|  |         borderRadius: BorderRadius.circular(12), | ||||||
|  |         child: Container( | ||||||
|  |           decoration: BoxDecoration( | ||||||
|  |             borderRadius: BorderRadius.circular(12), | ||||||
|  |             gradient: LinearGradient( | ||||||
|  |               colors: [ | ||||||
|  |                 stageColor.withOpacity(0.1), | ||||||
|  |                 Theme.of(context).colorScheme.surface, | ||||||
|  |               ], | ||||||
|  |               begin: Alignment.topLeft, | ||||||
|  |               end: Alignment.bottomRight, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|           child: Column( |           child: Column( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, |             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|             children: [ |             children: [ | ||||||
|               Row( |               Row( | ||||||
|             spacing: 8, |                 spacing: rowSpacing, | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.baseline, |                 crossAxisAlignment: CrossAxisAlignment.baseline, | ||||||
|                 mainAxisAlignment: MainAxisAlignment.spaceBetween, |                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|                 textBaseline: TextBaseline.alphabetic, |                 textBaseline: TextBaseline.alphabetic, | ||||||
|                 children: [ |                 children: [ | ||||||
|               Text( |                   Expanded( | ||||||
|  |                     child: Text( | ||||||
|                       'levelingProgressLevel'.tr(args: [level.toString()]), |                       'levelingProgressLevel'.tr(args: [level.toString()]), | ||||||
|                 style: GoogleFonts.robotoMono(), |                       style: TextStyle( | ||||||
|               ).fontSize(13).bold(), |                         color: stageColor, | ||||||
|  |                         fontWeight: FontWeight.bold, | ||||||
|  |                         fontSize: levelFontSize, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Row( | ||||||
|  |                     mainAxisSize: MainAxisSize.min, | ||||||
|  |                     children: [ | ||||||
|                       Text( |                       Text( | ||||||
|                 'levelingProgressExperience'.tr(args: [experience.toString()]), |                         'levelingStage$stage'.tr(), | ||||||
|                 style: GoogleFonts.robotoMono(), |                         style: TextStyle( | ||||||
|               ).fontSize(13), |                           color: stageColor.withOpacity(0.7), | ||||||
|  |                           fontWeight: FontWeight.w500, | ||||||
|  |                           fontSize: stageFontSize, | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       if (onTap != null) ...[ | ||||||
|  |                         const Gap(4), | ||||||
|  |                         Icon( | ||||||
|  |                           Icons.arrow_forward_ios, | ||||||
|  |                           size: isCompact ? 10 : 12, | ||||||
|  |                           color: stageColor.withOpacity(0.7), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|           const Gap(8), |                 ], | ||||||
|           Tooltip( |               ), | ||||||
|             message: '${(progress).toStringAsFixed(1)}%', |               Gap(gapSize), | ||||||
|  |               Row( | ||||||
|  |                 spacing: rowSpacing, | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                 children: [ | ||||||
|  |                   Expanded( | ||||||
|  |                     child: Tooltip( | ||||||
|  |                       message: '${progress.toStringAsFixed(1)}%', | ||||||
|                       child: LinearProgressIndicator( |                       child: LinearProgressIndicator( | ||||||
|               minHeight: 4, |                         minHeight: progressHeight, | ||||||
|               value: progress / 100, |                         value: progress, | ||||||
|               color: Theme.of(context).colorScheme.primary, |                         borderRadius: BorderRadius.circular(32), | ||||||
|               backgroundColor: |                         backgroundColor: Theme.of( | ||||||
|                   Theme.of(context).colorScheme.surfaceContainerHigh, |                           context, | ||||||
|  |                         ).colorScheme.surfaceContainerLow.withOpacity(0.75), | ||||||
|  |                         color: stageColor, | ||||||
|  |                         stopIndicatorRadius: 0, | ||||||
|  |                         trackGap: 0, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Text( | ||||||
|  |                     'levelingProgressExperience'.tr( | ||||||
|  |                       args: [experience.toString()], | ||||||
|  |                     ), | ||||||
|  |                     style: TextStyle( | ||||||
|  |                       color: Theme.of( | ||||||
|  |                         context, | ||||||
|  |                       ).colorScheme.onSurface.withOpacity(0.8), | ||||||
|  |                       fontSize: experienceFontSize, | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
|                 ], |                 ], | ||||||
|       ).padding(horizontal: 16, vertical: 12), |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: horizontalPadding, vertical: verticalPadding), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     return cardContent; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/account.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/pods/userinfo.dart'; | ||||||
| import 'package:island/screens/account/profile.dart'; | import 'package:island/screens/account/profile.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
|  | import 'package:island/utils/activity_utils.dart'; | ||||||
| import 'package:island/widgets/account/status_creation.dart'; | import 'package:island/widgets/account/status_creation.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,8 +15,31 @@ import 'package:styled_widget/styled_widget.dart'; | |||||||
|  |  | ||||||
| part 'status.g.dart'; | part 'status.g.dart'; | ||||||
|  |  | ||||||
|  | class CurrentAccountStatusNotifier extends StateNotifier<SnAccountStatus?> { | ||||||
|  |   CurrentAccountStatusNotifier() : super(null); | ||||||
|  |  | ||||||
|  |   void setStatus(SnAccountStatus status) { | ||||||
|  |     state = status; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void clearStatus() { | ||||||
|  |     state = null; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | final currentAccountStatusProvider = StateNotifierProvider<CurrentAccountStatusNotifier, SnAccountStatus?>((ref) { | ||||||
|  |   return CurrentAccountStatusNotifier(); | ||||||
|  | }); | ||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async { | Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async { | ||||||
|  |   final userInfo = ref.watch(userInfoProvider); | ||||||
|  |   if (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name)) { | ||||||
|  |     final local = ref.watch(currentAccountStatusProvider); | ||||||
|  |     if (local != null) { | ||||||
|  |       return local; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   final apiClient = ref.watch(apiClientProvider); |   final apiClient = ref.watch(apiClientProvider); | ||||||
|   try { |   try { | ||||||
|     final resp = await apiClient.get('/id/accounts/$uname/statuses'); |     final resp = await apiClient.get('/id/accounts/$uname/statuses'); | ||||||
| @@ -110,7 +135,11 @@ class AccountStatusWidget extends HookConsumerWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final status = ref.watch(accountStatusProvider(uname)); |     final userInfo = ref.watch(userInfoProvider); | ||||||
|  |     final localStatus = ref.watch(currentAccountStatusProvider); | ||||||
|  |     final status = (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name && localStatus != null)) | ||||||
|  |         ? AsyncValue.data(localStatus) | ||||||
|  |         : ref.watch(accountStatusProvider(uname)); | ||||||
|     final account = ref.watch(accountProvider(uname)); |     final account = ref.watch(accountProvider(uname)); | ||||||
|  |  | ||||||
|     return Padding( |     return Padding( | ||||||
| @@ -133,11 +162,32 @@ class AccountStatusWidget extends HookConsumerWidget { | |||||||
|             ).padding(right: 4), |             ).padding(right: 4), | ||||||
|           if (status.value?.isCustomized ?? false) |           if (status.value?.isCustomized ?? false) | ||||||
|             Flexible( |             Flexible( | ||||||
|  |               child: GestureDetector( | ||||||
|  |                 onLongPress: () { | ||||||
|  |                   showDialog( | ||||||
|  |                     context: context, | ||||||
|  |                     builder: (context) => AlertDialog( | ||||||
|  |                       title: Text('Activity Details'), | ||||||
|  |                       content: buildActivityDetails(status.value), | ||||||
|  |                       actions: [ | ||||||
|  |                         TextButton( | ||||||
|  |                           onPressed: () => Navigator.of(context).pop(), | ||||||
|  |                           child: Text('Close'), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 child: Tooltip( | ||||||
|  |                   richMessage: getActivityFullMessage(status.value), | ||||||
|                   child: Text( |                   child: Text( | ||||||
|                 status.value?.label ?? 'unknown'.tr(), |                     getActivityTitle(status.value?.label, status.value?.meta) ?? | ||||||
|  |                         'unknown'.tr(), | ||||||
|                     maxLines: 1, |                     maxLines: 1, | ||||||
|                     overflow: TextOverflow.ellipsis, |                     overflow: TextOverflow.ellipsis, | ||||||
|                   ), |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|             ) |             ) | ||||||
|           else |           else | ||||||
|             Flexible( |             Flexible( | ||||||
| @@ -148,7 +198,13 @@ class AccountStatusWidget extends HookConsumerWidget { | |||||||
|                     overflow: TextOverflow.ellipsis, |                     overflow: TextOverflow.ellipsis, | ||||||
|                   ).tr(), |                   ).tr(), | ||||||
|             ), |             ), | ||||||
|           if (!(status.value?.isOnline ?? false) && |           if (getActivitySubtitle(status.value?.meta) != null) | ||||||
|  |             Flexible( | ||||||
|  |               child: Text( | ||||||
|  |                 getActivitySubtitle(status.value?.meta)!, | ||||||
|  |               ).opacity(0.75), | ||||||
|  |             ) | ||||||
|  |           else if (!(status.value?.isOnline ?? false) && | ||||||
|               account.value?.profile.lastSeenAt != null) |               account.value?.profile.lastSeenAt != null) | ||||||
|             Flexible( |             Flexible( | ||||||
|               child: Text( |               child: Text( | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/pods/activity_rpc.dart'; | import 'package:island/pods/activity/activity_rpc.dart'; | ||||||
| import 'package:island/pods/websocket.dart'; | import 'package:island/pods/websocket.dart'; | ||||||
| import 'package:island/screens/tray_manager.dart'; | import 'package:island/screens/tray_manager.dart'; | ||||||
| import 'package:island/services/notify.dart'; | import 'package:island/services/notify.dart'; | ||||||
|   | |||||||
							
								
								
									
										363
									
								
								lib/widgets/attachment_uploader.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								lib/widgets/attachment_uploader.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,363 @@ | |||||||
|  | import 'dart:typed_data'; | ||||||
|  |  | ||||||
|  | import 'package:cross_file/cross_file.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/file.dart'; | ||||||
|  | import 'package:island/models/file_pool.dart'; | ||||||
|  | import 'package:island/pods/file_pool.dart'; | ||||||
|  | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
|  | import 'package:island/widgets/content/sheet.dart'; | ||||||
|  | import 'package:island/widgets/post/compose_shared.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | class AttachmentUploadConfig { | ||||||
|  |   final String poolId; | ||||||
|  |   final bool hasConstraints; | ||||||
|  |  | ||||||
|  |   const AttachmentUploadConfig({ | ||||||
|  |     required this.poolId, | ||||||
|  |     required this.hasConstraints, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class AttachmentUploaderSheet extends StatefulWidget { | ||||||
|  |   final WidgetRef ref; | ||||||
|  |   final ComposeState state; | ||||||
|  |   final int index; | ||||||
|  |  | ||||||
|  |   const AttachmentUploaderSheet({ | ||||||
|  |     super.key, | ||||||
|  |     required this.ref, | ||||||
|  |     required this.state, | ||||||
|  |     required this.index, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<AttachmentUploaderSheet> createState() => | ||||||
|  |       _AttachmentUploaderSheetState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> { | ||||||
|  |   String? selectedPoolId; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final attachment = widget.state.attachments.value[widget.index]; | ||||||
|  |  | ||||||
|  |     return SheetScaffold( | ||||||
|  |       titleText: 'uploadAttachment'.tr(), | ||||||
|  |       child: FutureBuilder<List<SnFilePool>>( | ||||||
|  |         future: widget.ref.read(poolsProvider.future), | ||||||
|  |         builder: (context, snapshot) { | ||||||
|  |           if (snapshot.connectionState == ConnectionState.waiting) { | ||||||
|  |             return const Center(child: CircularProgressIndicator()); | ||||||
|  |           } | ||||||
|  |           if (snapshot.hasError) { | ||||||
|  |             return Center(child: Text('errorLoadingPools'.tr())); | ||||||
|  |           } | ||||||
|  |           final pools = snapshot.data!; | ||||||
|  |           selectedPoolId ??= resolveDefaultPoolId(widget.ref, pools); | ||||||
|  |  | ||||||
|  |           return Column( | ||||||
|  |             children: [ | ||||||
|  |               Expanded( | ||||||
|  |                 child: SingleChildScrollView( | ||||||
|  |                   padding: const EdgeInsets.all(16), | ||||||
|  |                   child: Column( | ||||||
|  |                     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                     children: [ | ||||||
|  |                       DropdownButtonFormField<String>( | ||||||
|  |                         value: selectedPoolId, | ||||||
|  |                         items: | ||||||
|  |                             pools.map((pool) { | ||||||
|  |                               return DropdownMenuItem<String>( | ||||||
|  |                                 value: pool.id, | ||||||
|  |                                 child: Text(pool.name), | ||||||
|  |                               ); | ||||||
|  |                             }).toList(), | ||||||
|  |                         onChanged: (value) { | ||||||
|  |                           setState(() { | ||||||
|  |                             selectedPoolId = value; | ||||||
|  |                           }); | ||||||
|  |                         }, | ||||||
|  |                         decoration: InputDecoration( | ||||||
|  |                           labelText: 'selectPool'.tr(), | ||||||
|  |                           border: const OutlineInputBorder(), | ||||||
|  |                           hintText: 'choosePool'.tr(), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       const Gap(16), | ||||||
|  |                       FutureBuilder<int?>( | ||||||
|  |                         future: _getFileSize(attachment), | ||||||
|  |                         builder: (context, sizeSnapshot) { | ||||||
|  |                           if (!sizeSnapshot.hasData) { | ||||||
|  |                             return const SizedBox.shrink(); | ||||||
|  |                           } | ||||||
|  |                           final fileSize = sizeSnapshot.data!; | ||||||
|  |                           final selectedPool = pools.firstWhere( | ||||||
|  |                             (p) => p.id == selectedPoolId, | ||||||
|  |                           ); | ||||||
|  |  | ||||||
|  |                           // Check file size limit | ||||||
|  |                           final maxFileSize = | ||||||
|  |                               selectedPool.policyConfig?['max_file_size'] | ||||||
|  |                                   as int?; | ||||||
|  |                           final fileSizeExceeded = | ||||||
|  |                               maxFileSize != null && fileSize > maxFileSize; | ||||||
|  |  | ||||||
|  |                           // Check accepted types | ||||||
|  |                           final acceptTypes = | ||||||
|  |                               selectedPool.policyConfig?['accept_types'] | ||||||
|  |                                   as List?; | ||||||
|  |                           final mimeType = | ||||||
|  |                               attachment.data.mimeType ?? | ||||||
|  |                               ComposeLogic.getMimeTypeFromFileType( | ||||||
|  |                                 attachment.type, | ||||||
|  |                               ); | ||||||
|  |                           final typeAccepted = | ||||||
|  |                               acceptTypes == null || | ||||||
|  |                               acceptTypes.isEmpty || | ||||||
|  |                               acceptTypes.any( | ||||||
|  |                                 (type) => mimeType.startsWith(type), | ||||||
|  |                               ); | ||||||
|  |  | ||||||
|  |                           final hasIssues = fileSizeExceeded || !typeAccepted; | ||||||
|  |  | ||||||
|  |                           return Column( | ||||||
|  |                             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                             children: [ | ||||||
|  |                               if (hasIssues) ...[ | ||||||
|  |                                 Container( | ||||||
|  |                                   padding: const EdgeInsets.all(12), | ||||||
|  |                                   decoration: BoxDecoration( | ||||||
|  |                                     color: | ||||||
|  |                                         Theme.of( | ||||||
|  |                                           context, | ||||||
|  |                                         ).colorScheme.errorContainer, | ||||||
|  |                                     borderRadius: BorderRadius.circular(8), | ||||||
|  |                                   ), | ||||||
|  |                                   child: Column( | ||||||
|  |                                     crossAxisAlignment: | ||||||
|  |                                         CrossAxisAlignment.start, | ||||||
|  |                                     children: [ | ||||||
|  |                                       Row( | ||||||
|  |                                         children: [ | ||||||
|  |                                           Icon( | ||||||
|  |                                             Symbols.warning, | ||||||
|  |                                             size: 18, | ||||||
|  |                                             color: | ||||||
|  |                                                 Theme.of( | ||||||
|  |                                                   context, | ||||||
|  |                                                 ).colorScheme.error, | ||||||
|  |                                           ), | ||||||
|  |                                           const Gap(8), | ||||||
|  |                                           Text( | ||||||
|  |                                             'uploadConstraints'.tr(), | ||||||
|  |                                             style: Theme.of( | ||||||
|  |                                               context, | ||||||
|  |                                             ).textTheme.bodyMedium?.copyWith( | ||||||
|  |                                               color: | ||||||
|  |                                                   Theme.of( | ||||||
|  |                                                     context, | ||||||
|  |                                                   ).colorScheme.error, | ||||||
|  |                                               fontWeight: FontWeight.w600, | ||||||
|  |                                             ), | ||||||
|  |                                           ), | ||||||
|  |                                         ], | ||||||
|  |                                       ), | ||||||
|  |                                       if (fileSizeExceeded) ...[ | ||||||
|  |                                         const Gap(4), | ||||||
|  |                                         Text( | ||||||
|  |                                           'fileSizeExceeded'.tr( | ||||||
|  |                                             args: [ | ||||||
|  |                                               _formatFileSize(maxFileSize), | ||||||
|  |                                             ], | ||||||
|  |                                           ), | ||||||
|  |                                           style: Theme.of( | ||||||
|  |                                             context, | ||||||
|  |                                           ).textTheme.bodySmall?.copyWith( | ||||||
|  |                                             color: | ||||||
|  |                                                 Theme.of( | ||||||
|  |                                                   context, | ||||||
|  |                                                 ).colorScheme.error, | ||||||
|  |                                           ), | ||||||
|  |                                         ), | ||||||
|  |                                       ], | ||||||
|  |                                       if (!typeAccepted) ...[ | ||||||
|  |                                         const Gap(4), | ||||||
|  |                                         Text( | ||||||
|  |                                           'fileTypeNotAccepted'.tr(), | ||||||
|  |                                           style: Theme.of( | ||||||
|  |                                             context, | ||||||
|  |                                           ).textTheme.bodySmall?.copyWith( | ||||||
|  |                                             color: | ||||||
|  |                                                 Theme.of( | ||||||
|  |                                                   context, | ||||||
|  |                                                 ).colorScheme.error, | ||||||
|  |                                           ), | ||||||
|  |                                         ), | ||||||
|  |                                       ], | ||||||
|  |                                     ], | ||||||
|  |                                   ), | ||||||
|  |                                 ), | ||||||
|  |                                 const Gap(12), | ||||||
|  |                               ], | ||||||
|  |                               Row( | ||||||
|  |                                 spacing: 6, | ||||||
|  |                                 children: [ | ||||||
|  |                                   const Icon( | ||||||
|  |                                     Symbols.account_balance_wallet, | ||||||
|  |                                     size: 18, | ||||||
|  |                                   ), | ||||||
|  |                                   Expanded( | ||||||
|  |                                     child: Text( | ||||||
|  |                                       'quotaCostInfo'.tr( | ||||||
|  |                                         args: [ | ||||||
|  |                                           _formatQuotaCost( | ||||||
|  |                                             fileSize, | ||||||
|  |                                             selectedPool, | ||||||
|  |                                           ), | ||||||
|  |                                         ], | ||||||
|  |                                       ), | ||||||
|  |                                       style: | ||||||
|  |                                           Theme.of( | ||||||
|  |                                             context, | ||||||
|  |                                           ).textTheme.bodyMedium, | ||||||
|  |                                     ).fontSize(13), | ||||||
|  |                                   ), | ||||||
|  |                                 ], | ||||||
|  |                               ).padding(horizontal: 4), | ||||||
|  |                             ], | ||||||
|  |                           ); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                       const Gap(4), | ||||||
|  |                       Row( | ||||||
|  |                         spacing: 6, | ||||||
|  |                         children: [ | ||||||
|  |                           const Icon(Symbols.info, size: 18), | ||||||
|  |                           Text( | ||||||
|  |                             'attachmentPreview'.tr(), | ||||||
|  |                             style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |                           ).fontSize(13), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 4), | ||||||
|  |                       const Gap(8), | ||||||
|  |                       AttachmentPreview(item: attachment, isCompact: true), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               Padding( | ||||||
|  |                 padding: const EdgeInsets.all(16), | ||||||
|  |                 child: Row( | ||||||
|  |                   mainAxisAlignment: MainAxisAlignment.end, | ||||||
|  |                   children: [ | ||||||
|  |                     TextButton.icon( | ||||||
|  |                       onPressed: () => Navigator.pop(context), | ||||||
|  |                       icon: const Icon(Symbols.close), | ||||||
|  |                       label: Text('cancel').tr(), | ||||||
|  |                     ), | ||||||
|  |                     const Gap(8), | ||||||
|  |                     TextButton.icon( | ||||||
|  |                       onPressed: () => _confirmUpload(), | ||||||
|  |                       icon: const Icon(Symbols.upload), | ||||||
|  |                       label: Text('upload').tr(), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<AttachmentUploadConfig?> _getUploadConfig() async { | ||||||
|  |     final attachment = widget.state.attachments.value[widget.index]; | ||||||
|  |     final fileSize = await _getFileSize(attachment); | ||||||
|  |  | ||||||
|  |     if (fileSize == null) return null; | ||||||
|  |  | ||||||
|  |     // Get the selected pool to check constraints | ||||||
|  |     final pools = await widget.ref.read(poolsProvider.future); | ||||||
|  |     final selectedPool = pools.firstWhere((p) => p.id == selectedPoolId); | ||||||
|  |  | ||||||
|  |     // Check constraints | ||||||
|  |     final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?; | ||||||
|  |     final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize; | ||||||
|  |  | ||||||
|  |     final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?; | ||||||
|  |     final mimeType = | ||||||
|  |         attachment.data.mimeType ?? | ||||||
|  |         ComposeLogic.getMimeTypeFromFileType(attachment.type); | ||||||
|  |     final typeAccepted = | ||||||
|  |         acceptTypes == null || | ||||||
|  |         acceptTypes.isEmpty || | ||||||
|  |         acceptTypes.any((type) => mimeType.startsWith(type)); | ||||||
|  |  | ||||||
|  |     final hasConstraints = fileSizeExceeded || !typeAccepted; | ||||||
|  |  | ||||||
|  |     return AttachmentUploadConfig( | ||||||
|  |       poolId: selectedPoolId!, | ||||||
|  |       hasConstraints: hasConstraints, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> _confirmUpload() async { | ||||||
|  |     final config = await _getUploadConfig(); | ||||||
|  |     if (config != null && mounted) { | ||||||
|  |       Navigator.pop(context, config); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<int?> _getFileSize(UniversalFile attachment) async { | ||||||
|  |     if (attachment.data is XFile) { | ||||||
|  |       try { | ||||||
|  |         return await (attachment.data as XFile).length(); | ||||||
|  |       } catch (e) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |     } else if (attachment.data is SnCloudFile) { | ||||||
|  |       return (attachment.data as SnCloudFile).size; | ||||||
|  |     } else if (attachment.data is List<int>) { | ||||||
|  |       return (attachment.data as List<int>).length; | ||||||
|  |     } else if (attachment.data is Uint8List) { | ||||||
|  |       return (attachment.data as Uint8List).length; | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String _formatNumber(int number) { | ||||||
|  |     if (number >= 1000000) { | ||||||
|  |       return '${(number / 1000000).toStringAsFixed(1)}M'; | ||||||
|  |     } else if (number >= 1000) { | ||||||
|  |       return '${(number / 1000).toStringAsFixed(1)}K'; | ||||||
|  |     } else { | ||||||
|  |       return number.toString(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String _formatFileSize(int bytes) { | ||||||
|  |     if (bytes >= 1073741824) { | ||||||
|  |       return '${(bytes / 1073741824).toStringAsFixed(1)} GB'; | ||||||
|  |     } else if (bytes >= 1048576) { | ||||||
|  |       return '${(bytes / 1048576).toStringAsFixed(1)} MB'; | ||||||
|  |     } else if (bytes >= 1024) { | ||||||
|  |       return '${(bytes / 1024).toStringAsFixed(1)} KB'; | ||||||
|  |     } else { | ||||||
|  |       return '$bytes bytes'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String _formatQuotaCost(int fileSize, SnFilePool pool) { | ||||||
|  |     final costMultiplier = pool.billingConfig?['cost_multiplier'] ?? 1.0; | ||||||
|  |     final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round(); | ||||||
|  |     return _formatNumber(quotaCost); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										330
									
								
								lib/widgets/chat/chat_input.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								lib/widgets/chat/chat_input.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,330 @@ | |||||||
|  | import "dart:async"; | ||||||
|  | import "dart:io"; | ||||||
|  | import "package:easy_localization/easy_localization.dart"; | ||||||
|  | import "package:flutter/foundation.dart"; | ||||||
|  | import "package:flutter/material.dart"; | ||||||
|  | import "package:flutter/services.dart"; | ||||||
|  | import "package:flutter_hooks/flutter_hooks.dart"; | ||||||
|  | import "package:gap/gap.dart"; | ||||||
|  | import "package:hooks_riverpod/hooks_riverpod.dart"; | ||||||
|  | import "package:image_picker/image_picker.dart"; | ||||||
|  | import "package:island/models/chat.dart"; | ||||||
|  | import "package:island/models/file.dart"; | ||||||
|  | import "package:island/pods/config.dart"; | ||||||
|  | import "package:island/widgets/content/attachment_preview.dart"; | ||||||
|  | import "package:material_symbols_icons/material_symbols_icons.dart"; | ||||||
|  | import "package:pasteboard/pasteboard.dart"; | ||||||
|  | import "package:styled_widget/styled_widget.dart"; | ||||||
|  | import "package:material_symbols_icons/symbols.dart"; | ||||||
|  | import "package:island/widgets/stickers/picker.dart"; | ||||||
|  |  | ||||||
|  | class ChatInput extends HookConsumerWidget { | ||||||
|  |   final TextEditingController messageController; | ||||||
|  |   final SnChatRoom chatRoom; | ||||||
|  |   final VoidCallback onSend; | ||||||
|  |   final VoidCallback onClear; | ||||||
|  |   final Function(bool isPhoto) onPickFile; | ||||||
|  |   final SnChatMessage? messageReplyingTo; | ||||||
|  |   final SnChatMessage? messageForwardingTo; | ||||||
|  |   final SnChatMessage? messageEditingTo; | ||||||
|  |   final List<UniversalFile> attachments; | ||||||
|  |   final Function(int) onUploadAttachment; | ||||||
|  |   final Function(int) onDeleteAttachment; | ||||||
|  |   final Function(int, int) onMoveAttachment; | ||||||
|  |   final Function(List<UniversalFile>) onAttachmentsChanged; | ||||||
|  |   final Map<String, Map<int, double>> attachmentProgress; | ||||||
|  |  | ||||||
|  |   const ChatInput({ | ||||||
|  |     super.key, | ||||||
|  |     required this.messageController, | ||||||
|  |     required this.chatRoom, | ||||||
|  |     required this.onSend, | ||||||
|  |     required this.onClear, | ||||||
|  |     required this.onPickFile, | ||||||
|  |     required this.messageReplyingTo, | ||||||
|  |     required this.messageForwardingTo, | ||||||
|  |     required this.messageEditingTo, | ||||||
|  |     required this.attachments, | ||||||
|  |     required this.onUploadAttachment, | ||||||
|  |     required this.onDeleteAttachment, | ||||||
|  |     required this.onMoveAttachment, | ||||||
|  |     required this.onAttachmentsChanged, | ||||||
|  |     required this.attachmentProgress, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final inputFocusNode = useFocusNode(); | ||||||
|  |  | ||||||
|  |     final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend; | ||||||
|  |  | ||||||
|  |     final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS); | ||||||
|  |  | ||||||
|  |     void send() { | ||||||
|  |       onSend.call(); | ||||||
|  |       WidgetsBinding.instance.addPostFrameCallback((_) { | ||||||
|  |         inputFocusNode.requestFocus(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Future<void> handlePaste() async { | ||||||
|  |       final clipboard = await Pasteboard.image; | ||||||
|  |       if (clipboard == null) return; | ||||||
|  |  | ||||||
|  |       onAttachmentsChanged([ | ||||||
|  |         ...attachments, | ||||||
|  |         UniversalFile( | ||||||
|  |           data: XFile.fromData(clipboard, mimeType: "image/jpeg"), | ||||||
|  |           type: UniversalFileType.image, | ||||||
|  |         ), | ||||||
|  |       ]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     void handleKeyPress( | ||||||
|  |       BuildContext context, | ||||||
|  |       WidgetRef ref, | ||||||
|  |       RawKeyEvent event, | ||||||
|  |     ) { | ||||||
|  |       if (event is! RawKeyDownEvent) return; | ||||||
|  |  | ||||||
|  |       final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; | ||||||
|  |       final isModifierPressed = event.isMetaPressed || event.isControlPressed; | ||||||
|  |  | ||||||
|  |       if (isPaste && isModifierPressed) { | ||||||
|  |         handlePaste(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend; | ||||||
|  |       final isEnter = event.logicalKey == LogicalKeyboardKey.enter; | ||||||
|  |  | ||||||
|  |       if (isEnter) { | ||||||
|  |         if (enterToSend && !isModifierPressed) { | ||||||
|  |           send(); | ||||||
|  |         } else if (!enterToSend && isModifierPressed) { | ||||||
|  |           send(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return Material( | ||||||
|  |       elevation: 8, | ||||||
|  |       color: Theme.of(context).colorScheme.surface, | ||||||
|  |       child: Column( | ||||||
|  |         children: [ | ||||||
|  |           if (attachments.isNotEmpty) | ||||||
|  |             SizedBox( | ||||||
|  |               height: 280, | ||||||
|  |               child: ListView.separated( | ||||||
|  |                 padding: EdgeInsets.symmetric(horizontal: 12), | ||||||
|  |                 scrollDirection: Axis.horizontal, | ||||||
|  |                 itemCount: attachments.length, | ||||||
|  |                 itemBuilder: (context, idx) { | ||||||
|  |                   return SizedBox( | ||||||
|  |                     height: 280, | ||||||
|  |                     width: 280, | ||||||
|  |                     child: AttachmentPreview( | ||||||
|  |                       item: attachments[idx], | ||||||
|  |                       progress: attachmentProgress['chat-upload']?[idx], | ||||||
|  |                       onRequestUpload: () => onUploadAttachment(idx), | ||||||
|  |                       onDelete: () => onDeleteAttachment(idx), | ||||||
|  |                       onUpdate: (value) { | ||||||
|  |                         attachments[idx] = value; | ||||||
|  |                         onAttachmentsChanged(attachments); | ||||||
|  |                       }, | ||||||
|  |                       onMove: (delta) => onMoveAttachment(idx, delta), | ||||||
|  |                     ), | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 separatorBuilder: (_, _) => const Gap(8), | ||||||
|  |               ), | ||||||
|  |             ).padding(top: 12), | ||||||
|  |           if (messageReplyingTo != null || | ||||||
|  |               messageForwardingTo != null || | ||||||
|  |               messageEditingTo != null) | ||||||
|  |             Container( | ||||||
|  |               padding: const EdgeInsets.symmetric(horizontal: 16), | ||||||
|  |               decoration: BoxDecoration( | ||||||
|  |                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|  |                 borderRadius: BorderRadius.circular(8), | ||||||
|  |               ), | ||||||
|  |               margin: const EdgeInsets.only(left: 8, right: 8, top: 8), | ||||||
|  |               child: Row( | ||||||
|  |                 children: [ | ||||||
|  |                   Icon( | ||||||
|  |                     messageReplyingTo != null | ||||||
|  |                         ? Symbols.reply | ||||||
|  |                         : messageForwardingTo != null | ||||||
|  |                         ? Symbols.forward | ||||||
|  |                         : Symbols.edit, | ||||||
|  |                     size: 20, | ||||||
|  |                     color: Theme.of(context).colorScheme.primary, | ||||||
|  |                   ), | ||||||
|  |                   const Gap(8), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: Text( | ||||||
|  |                       messageReplyingTo != null | ||||||
|  |                           ? 'Replying to ${messageReplyingTo?.sender.account.nick}' | ||||||
|  |                           : messageForwardingTo != null | ||||||
|  |                           ? 'Forwarding message' | ||||||
|  |                           : 'Editing message', | ||||||
|  |                       style: Theme.of(context).textTheme.bodySmall, | ||||||
|  |                       maxLines: 1, | ||||||
|  |                       overflow: TextOverflow.ellipsis, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   IconButton( | ||||||
|  |                     icon: const Icon(Icons.close, size: 20), | ||||||
|  |                     onPressed: onClear, | ||||||
|  |                     padding: EdgeInsets.zero, | ||||||
|  |                     style: ButtonStyle( | ||||||
|  |                       minimumSize: WidgetStatePropertyAll(Size(28, 28)), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           Padding( | ||||||
|  |             padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), | ||||||
|  |             child: Row( | ||||||
|  |               children: [ | ||||||
|  |                 Row( | ||||||
|  |                   mainAxisSize: MainAxisSize.min, | ||||||
|  |                   children: [ | ||||||
|  |                     IconButton( | ||||||
|  |                       tooltip: 'stickers'.tr(), | ||||||
|  |                       icon: const Icon(Symbols.add_reaction), | ||||||
|  |                       onPressed: () { | ||||||
|  |                         final size = MediaQuery.of(context).size; | ||||||
|  |                         showStickerPickerPopover( | ||||||
|  |                           context, | ||||||
|  |                           Offset( | ||||||
|  |                             20, | ||||||
|  |                             size.height - | ||||||
|  |                                 480 - | ||||||
|  |                                 MediaQuery.of(context).padding.bottom, | ||||||
|  |                           ), | ||||||
|  |                           onPick: (placeholder) { | ||||||
|  |                             // Insert placeholder at current cursor position | ||||||
|  |                             final text = messageController.text; | ||||||
|  |                             final selection = messageController.selection; | ||||||
|  |                             final start = | ||||||
|  |                                 selection.start >= 0 | ||||||
|  |                                     ? selection.start | ||||||
|  |                                     : text.length; | ||||||
|  |                             final end = | ||||||
|  |                                 selection.end >= 0 | ||||||
|  |                                     ? selection.end | ||||||
|  |                                     : text.length; | ||||||
|  |                             final newText = text.replaceRange( | ||||||
|  |                               start, | ||||||
|  |                               end, | ||||||
|  |                               placeholder, | ||||||
|  |                             ); | ||||||
|  |                             messageController.value = TextEditingValue( | ||||||
|  |                               text: newText, | ||||||
|  |                               selection: TextSelection.collapsed( | ||||||
|  |                                 offset: start + placeholder.length, | ||||||
|  |                               ), | ||||||
|  |                             ); | ||||||
|  |                           }, | ||||||
|  |                         ); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                     PopupMenuButton( | ||||||
|  |                       icon: const Icon(Symbols.photo_library), | ||||||
|  |                       itemBuilder: | ||||||
|  |                           (context) => [ | ||||||
|  |                             PopupMenuItem( | ||||||
|  |                               onTap: () => onPickFile(true), | ||||||
|  |                               child: Row( | ||||||
|  |                                 spacing: 12, | ||||||
|  |                                 children: [ | ||||||
|  |                                   const Icon(Symbols.photo), | ||||||
|  |                                   Text('addPhoto').tr(), | ||||||
|  |                                 ], | ||||||
|  |                               ), | ||||||
|  |                             ), | ||||||
|  |                             PopupMenuItem( | ||||||
|  |                               onTap: () => onPickFile(false), | ||||||
|  |                               child: Row( | ||||||
|  |                                 spacing: 12, | ||||||
|  |                                 children: [ | ||||||
|  |                                   const Icon(Symbols.video_call), | ||||||
|  |                                   Text('addVideo').tr(), | ||||||
|  |                                 ], | ||||||
|  |                               ), | ||||||
|  |                             ), | ||||||
|  |                           ], | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |                 Expanded( | ||||||
|  |                   child: RawKeyboardListener( | ||||||
|  |                     focusNode: FocusNode(), | ||||||
|  |                     onKey: (event) => handleKeyPress(context, ref, event), | ||||||
|  |                     child: TextField( | ||||||
|  |                       focusNode: inputFocusNode, | ||||||
|  |                       controller: messageController, | ||||||
|  |                       onSubmitted: | ||||||
|  |                           (enterToSend && isMobile) | ||||||
|  |                               ? (_) { | ||||||
|  |                                 send(); | ||||||
|  |                               } | ||||||
|  |                               : null, | ||||||
|  |                       keyboardType: | ||||||
|  |                           (enterToSend && isMobile) | ||||||
|  |                               ? TextInputType.text | ||||||
|  |                               : TextInputType.multiline, | ||||||
|  |                       textInputAction: TextInputAction.send, | ||||||
|  |                       inputFormatters: [ | ||||||
|  |                         if (enterToSend && !isMobile) | ||||||
|  |                           TextInputFormatter.withFunction((oldValue, newValue) { | ||||||
|  |                             if (newValue.text.endsWith('\n')) { | ||||||
|  |                               return oldValue; | ||||||
|  |                             } | ||||||
|  |                             return newValue; | ||||||
|  |                           }), | ||||||
|  |                       ], | ||||||
|  |                       decoration: InputDecoration( | ||||||
|  |                         hintText: | ||||||
|  |                             (chatRoom.type == 1 && chatRoom.name == null) | ||||||
|  |                                 ? 'chatDirectMessageHint'.tr( | ||||||
|  |                                   args: [ | ||||||
|  |                                     chatRoom.members! | ||||||
|  |                                         .map((e) => e.account.nick) | ||||||
|  |                                         .join(', '), | ||||||
|  |                                   ], | ||||||
|  |                                 ) | ||||||
|  |                                 : 'chatMessageHint'.tr(args: [chatRoom.name!]), | ||||||
|  |                         border: InputBorder.none, | ||||||
|  |                         isDense: true, | ||||||
|  |                         contentPadding: const EdgeInsets.symmetric( | ||||||
|  |                           horizontal: 12, | ||||||
|  |                           vertical: 4, | ||||||
|  |                         ), | ||||||
|  |                         counterText: | ||||||
|  |                             messageController.text.length > 1024 | ||||||
|  |                                 ? '${messageController.text.length}/4096' | ||||||
|  |                                 : null, | ||||||
|  |                       ), | ||||||
|  |                       maxLines: 3, | ||||||
|  |                       minLines: 1, | ||||||
|  |                       onTapOutside: | ||||||
|  |                           (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 IconButton( | ||||||
|  |                   icon: const Icon(Icons.send), | ||||||
|  |                   color: Theme.of(context).colorScheme.primary, | ||||||
|  |                   onPressed: send, | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ).padding(bottom: MediaQuery.of(context).padding.bottom), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										172
									
								
								lib/widgets/chat/message_content.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								lib/widgets/chat/message_content.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | import 'dart:math' as math; | ||||||
|  |  | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:island/models/chat.dart'; | ||||||
|  | import 'package:island/pods/call.dart'; | ||||||
|  | import 'package:island/widgets/content/markdown.dart'; | ||||||
|  | import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||||
|  | import 'package:pretty_diff_text/pretty_diff_text.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | class MessageContent extends StatelessWidget { | ||||||
|  |   final SnChatMessage item; | ||||||
|  |   final String? translatedText; | ||||||
|  |  | ||||||
|  |   const MessageContent({super.key, required this.item, this.translatedText}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (item.type == 'messages.delete' || item.deletedAt != null) { | ||||||
|  |       return Row( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |         children: [ | ||||||
|  |           Icon( | ||||||
|  |             Symbols.delete, | ||||||
|  |             size: 16, | ||||||
|  |             color: Theme.of( | ||||||
|  |               context, | ||||||
|  |             ).colorScheme.onSurfaceVariant.withOpacity(0.6), | ||||||
|  |           ), | ||||||
|  |           const Gap(4), | ||||||
|  |           Text( | ||||||
|  |             item.content ?? 'Deleted a message', | ||||||
|  |             style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||||
|  |               fontSize: 13, | ||||||
|  |               color: Theme.of( | ||||||
|  |                 context, | ||||||
|  |               ).colorScheme.onSurfaceVariant.withOpacity(0.6), | ||||||
|  |               fontStyle: FontStyle.italic, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     switch (item.type) { | ||||||
|  |       case 'call.start': | ||||||
|  |       case 'call.ended': | ||||||
|  |         return _MessageContentCall( | ||||||
|  |           isEnded: item.type == 'call.ended', | ||||||
|  |           duration: item.meta['duration']?.toDouble(), | ||||||
|  |         ); | ||||||
|  |       case 'messages.update': | ||||||
|  |       case 'messages.update.links': | ||||||
|  |         return Row( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |           children: [ | ||||||
|  |             Icon( | ||||||
|  |               Symbols.edit, | ||||||
|  |               size: 16, | ||||||
|  |               color: Theme.of( | ||||||
|  |                 context, | ||||||
|  |               ).colorScheme.onSurfaceVariant.withOpacity(0.6), | ||||||
|  |             ), | ||||||
|  |             const Gap(4), | ||||||
|  |             if (item.meta['previous_content'] is String) | ||||||
|  |               PrettyDiffText( | ||||||
|  |                 oldText: item.meta['previous_content'], | ||||||
|  |                 newText: item.content ?? 'Edited a message', | ||||||
|  |                 defaultTextStyle: Theme.of( | ||||||
|  |                   context, | ||||||
|  |                 ).textTheme.bodyMedium!.copyWith( | ||||||
|  |                   color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|  |                 ), | ||||||
|  |                 addedTextStyle: TextStyle( | ||||||
|  |                   backgroundColor: Theme.of( | ||||||
|  |                     context, | ||||||
|  |                   ).colorScheme.primaryFixedDim.withOpacity(0.4), | ||||||
|  |                 ), | ||||||
|  |                 deletedTextStyle: TextStyle( | ||||||
|  |                   decoration: TextDecoration.lineThrough, | ||||||
|  |                   color: Theme.of( | ||||||
|  |                     context, | ||||||
|  |                   ).colorScheme.onSurfaceVariant.withOpacity(0.7), | ||||||
|  |                 ), | ||||||
|  |               ) | ||||||
|  |             else | ||||||
|  |               Text( | ||||||
|  |                 item.content ?? 'Edited a message', | ||||||
|  |                 style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||||
|  |                   color: Theme.of( | ||||||
|  |                     context, | ||||||
|  |                   ).colorScheme.onSurfaceVariant.withOpacity(0.6), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |           ], | ||||||
|  |         ); | ||||||
|  |       case 'text': | ||||||
|  |       default: | ||||||
|  |         return Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             MarkdownTextContent( | ||||||
|  |               content: item.content ?? '*${item.type} has no content*', | ||||||
|  |               isSelectable: true, | ||||||
|  |               linesMargin: EdgeInsets.zero, | ||||||
|  |             ), | ||||||
|  |             if (translatedText?.isNotEmpty ?? false) | ||||||
|  |               ...([ | ||||||
|  |                 ConstrainedBox( | ||||||
|  |                   constraints: BoxConstraints( | ||||||
|  |                     maxWidth: math.min( | ||||||
|  |                       280, | ||||||
|  |                       MediaQuery.of(context).size.width * 0.4, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   child: Row( | ||||||
|  |                     mainAxisSize: MainAxisSize.min, | ||||||
|  |                     children: [ | ||||||
|  |                       Text('translated').tr().fontSize(11).opacity(0.75), | ||||||
|  |                       const Gap(8), | ||||||
|  |                       Flexible(child: Divider()), | ||||||
|  |                     ], | ||||||
|  |                   ).padding(vertical: 4), | ||||||
|  |                 ), | ||||||
|  |                 MarkdownTextContent( | ||||||
|  |                   content: translatedText!, | ||||||
|  |                   isSelectable: true, | ||||||
|  |                   linesMargin: EdgeInsets.zero, | ||||||
|  |                 ), | ||||||
|  |               ]), | ||||||
|  |           ], | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static bool hasContent(SnChatMessage item) { | ||||||
|  |     return item.type != 'text' || (item.content?.isNotEmpty ?? false); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _MessageContentCall extends StatelessWidget { | ||||||
|  |   final bool isEnded; | ||||||
|  |   final double? duration; | ||||||
|  |  | ||||||
|  |   const _MessageContentCall({required this.isEnded, this.duration}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Row( | ||||||
|  |       mainAxisSize: MainAxisSize.min, | ||||||
|  |       children: [ | ||||||
|  |         Icon( | ||||||
|  |           isEnded ? Symbols.call_end : Symbols.phone_in_talk, | ||||||
|  |           size: 16, | ||||||
|  |           color: Theme.of(context).colorScheme.primary, | ||||||
|  |         ), | ||||||
|  |         Gap(4), | ||||||
|  |         Text( | ||||||
|  |           isEnded | ||||||
|  |               ? 'Call ended after ${formatDuration(Duration(seconds: duration!.toInt()))}' | ||||||
|  |               : 'Call started', | ||||||
|  |           style: TextStyle(color: Theme.of(context).colorScheme.primary), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								lib/widgets/chat/message_indicators.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								lib/widgets/chat/message_indicators.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/database/message.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | class MessageIndicators extends StatelessWidget { | ||||||
|  |   final DateTime? editedAt; | ||||||
|  |   final MessageStatus? status; | ||||||
|  |   final bool isCurrentUser; | ||||||
|  |   final Color textColor; | ||||||
|  |  | ||||||
|  |   const MessageIndicators({ | ||||||
|  |     super.key, | ||||||
|  |     this.editedAt, | ||||||
|  |     this.status, | ||||||
|  |     required this.isCurrentUser, | ||||||
|  |     required this.textColor, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Row( | ||||||
|  |       spacing: 4, | ||||||
|  |       mainAxisSize: MainAxisSize.min, | ||||||
|  |       children: [ | ||||||
|  |         if (editedAt != null) | ||||||
|  |           Text( | ||||||
|  |             'edited'.tr().toLowerCase(), | ||||||
|  |             style: TextStyle(fontSize: 11, color: textColor.withOpacity(0.7)), | ||||||
|  |           ), | ||||||
|  |         if (isCurrentUser && status != null) | ||||||
|  |           _buildStatusIcon( | ||||||
|  |             context, | ||||||
|  |             status!, | ||||||
|  |             textColor.withOpacity(0.7), | ||||||
|  |           ).padding(bottom: 3), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildStatusIcon( | ||||||
|  |     BuildContext context, | ||||||
|  |     MessageStatus status, | ||||||
|  |     Color textColor, | ||||||
|  |   ) { | ||||||
|  |     switch (status) { | ||||||
|  |       case MessageStatus.pending: | ||||||
|  |         return Icon(Icons.access_time, size: 12, color: textColor); | ||||||
|  |       case MessageStatus.sent: | ||||||
|  |         return Icon(Icons.check, size: 12, color: textColor); | ||||||
|  |       case MessageStatus.failed: | ||||||
|  |         return Consumer( | ||||||
|  |           builder: | ||||||
|  |               (context, ref, _) => GestureDetector( | ||||||
|  |                 onTap: () { | ||||||
|  |                   // This would need to be passed in or accessed differently | ||||||
|  |                   // For now, just show the error icon | ||||||
|  |                 }, | ||||||
|  |                 child: const Icon( | ||||||
|  |                   Icons.error_outline, | ||||||
|  |                   size: 12, | ||||||
|  |                   color: Colors.red, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user