Compare commits
	
		
			57 Commits
		
	
	
		
			2.3.2+75
			...
			e16bc80eea
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e16bc80eea | |||
| a4f6e8af56 | |||
| 060a97f5ec | |||
| 92f7e92018 | |||
| 5c483bd3b8 | |||
| 1c510d63fe | |||
| 115cb4adc1 | |||
| 54c098c274 | |||
| 29731728cd | |||
| 9e8882c580 | |||
| 6042e57e7a | |||
| 6235e736b9 | |||
| e075804782 | |||
| d40a6ca1c4 | |||
| 5ac657e526 | |||
| 97ddc18b8e | |||
| b835c8edea | |||
| 288c0399f9 | |||
| 1478933cf1 | |||
| 93c6fa6e53 | |||
| ce6e9c185a | |||
| cdaa8cfe58 | |||
| 76d8cd943d | |||
| d6f3ffc655 | |||
| 5a6b841253 | |||
| cb2de52bee | |||
| 64e2644745 | |||
| 56711889ab | |||
| 4f47cd2c0c | |||
| 2b61c372f5 | |||
| 73777fe74e | |||
| 33a4bd7e71 | |||
| 17e6b81f76 | |||
| 22fde6b400 | |||
| 6e03a00280 | |||
| 72e6a6a1f6 | |||
| 66aef44281 | |||
| 7bb73c80b0 | |||
| d043ef2410 | |||
| 1d0e2f7591 | |||
| e9ef28d764 | |||
| 289aa17a7a | |||
| 93f41bb523 | |||
| 09ec9d4a0c | |||
| 1153fbdeee | |||
| e933058338 | |||
| ae9743c84f | |||
| 32bf834108 | |||
| 1b41c847a6 | |||
| b1af6c2c97 | |||
| 8e76ff3f84 | |||
| bd26602299 | |||
| 52ab1d0d10 | |||
| f746e06f65 | |||
| d11069a2be | |||
| d6dc487d9e | |||
| a07c7cdede | 
							
								
								
									
										11
									
								
								api/Nexus/Check Status.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/Nexus/Check Status.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| meta { | ||||
|   name: Check Status | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/directory/status | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
							
								
								
									
										11
									
								
								api/Nexus/List Services.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/Nexus/List Services.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| meta { | ||||
|   name: List Services | ||||
|   type: http | ||||
|   seq: 2 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/directory/services | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
							
								
								
									
										18
									
								
								api/Passport/Deal Abuse Report.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/Passport/Deal Abuse Report.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| meta { | ||||
|   name: Deal Abuse Report | ||||
|   type: http | ||||
|   seq: 3 | ||||
| } | ||||
|  | ||||
| put { | ||||
|   url: {{endpoint}}/cgi/id/reports/abuse/3/status | ||||
|   body: json | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "status": "processed", | ||||
|     "message": "相关附件已经进行评级处理,未来会将该项权限下放到帖主以及社区成员。" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Bold.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Bold.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Italic.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Italic.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Regular.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Regular.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -153,6 +153,11 @@ | ||||
|   "publisherRunBy": "Run by {}", | ||||
|   "fieldPublisherBelongToRealm": "Belongs to", | ||||
|   "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm", | ||||
|   "writePost": "Compose", | ||||
|   "postTypeStory": "Story", | ||||
|   "postTypeArticle": "Article", | ||||
|   "postTypeQuestion": "Question", | ||||
|   "postTypeVideo": "Video", | ||||
|   "writePostTypeStory": "Post a story", | ||||
|   "writePostTypeArticle": "Write an article", | ||||
|   "writePostTypeQuestion": "Ask a question", | ||||
| @@ -203,6 +208,11 @@ | ||||
|     "other": "{} comments" | ||||
|   }, | ||||
|   "settingsAppearance": "Appearance", | ||||
|   "settingsCustomFonts": "Custom Fonts", | ||||
|   "settingsCustomFontsDescription": "Set custom fonts for the application.", | ||||
|   "settingsCustomFontFamily": "Custom Font Family", | ||||
|   "settingsCustomFontFamilyHint": "Use comma to separate fonts, higher priority comes first", | ||||
|   "settingsCustomFontApplied": "Custom font has been applied.", | ||||
|   "settingsDisplayLanguage": "Display Language", | ||||
|   "settingsDisplayLanguageDescription": "Set the application language.", | ||||
|   "settingsDisplayLanguageSystem": "Follow System", | ||||
| @@ -512,8 +522,13 @@ | ||||
|   "accountBirthday": "Born on {}", | ||||
|   "accountBadge": "Badge", | ||||
|   "accountCheckInNoRecords": "No check-in records", | ||||
|   "badgeCompanyStaff": "Solsynth Staff", | ||||
|   "badgeCompanyStaff": "Staff", | ||||
|   "badgeSiteMigration": "Solar Network Native", | ||||
|   "badgeCommunitySurvey": "Survey Participant", | ||||
|   "badgeCommunityVerified": "Verified User", | ||||
|   "badgeCommunityContributor": "Great Contributor", | ||||
|   "badgeSiteAnniversary": "Anniversary", | ||||
|   "badgeUserBirthday": "Birthday", | ||||
|   "accountStatus": "Status", | ||||
|   "accountStatusOnline": "Online", | ||||
|   "accountStatusOffline": "Offline", | ||||
| @@ -722,5 +737,37 @@ | ||||
|   "trayMenuMuteNotification": "Do Not Disturb", | ||||
|   "update": "Update", | ||||
|   "forceUpdate": "Force Update", | ||||
|   "forceUpdateDescription": "Force to show the application update popup, even the new version is not available." | ||||
|   "forceUpdateDescription": "Force to show the application update popup, even the new version is not available.", | ||||
|   "debugLogging": "Runtime Logs", | ||||
|   "runtimeLogsOpen": "Open Logs", | ||||
|   "runtimeLogsDescription": "Show the runtime logs to help debugging.", | ||||
|   "signinResetPasswordHint": "Please enter the username / email address to help us to find your account and reset your password.", | ||||
|   "cacheSize": "Cache Size", | ||||
|   "cacheDelete": "Clean Cache", | ||||
|   "cacheDeleteDescription": "Remove the cached images and other resources from your disk, the content will be downloaded from server again.", | ||||
|   "cacheDeleted": "All cache has been cleaned up.", | ||||
|   "userNoDescription": "No description.", | ||||
|   "fieldTimeZone": "Time Zone", | ||||
|   "fieldGender": "Gender", | ||||
|   "fieldPronouns": "Pronouns", | ||||
|   "fieldLocation": "Location", | ||||
|   "fieldLinks": "Links", | ||||
|   "fieldLinkName": "Name", | ||||
|   "fieldLinkUrl": "URL", | ||||
|   "screenAccountBadges": "Badges", | ||||
|   "accountBadges": "Badges", | ||||
|   "accountBadgesDescription": "View and manage your badges.", | ||||
|   "badgeActivated": "Activated badge {}.", | ||||
|   "viewDetailedAttachment": "Details", | ||||
|   "screenKeyPairs": "Key Pairs", | ||||
|   "accountKeyPairs": "Key Pairs", | ||||
|   "accountKeyPairsDescription": "Manage the key pairs which used to encrypt messages.", | ||||
|   "enrollNewKeyPair": "Enroll New One", | ||||
|   "enrollNewKeyPairDescription": "Generate a new key pair.", | ||||
|   "keyPairHasPrivateKey": "With private key", | ||||
|   "decrypting": "Decrypting……", | ||||
|   "decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online", | ||||
|   "messageUnablePreview": "Unable preview", | ||||
|   "messageUnablePreviewEncrypted": "Unable preview encrypted message", | ||||
|   "postViewInGlobalDescription": "Do not view the post in the specific realm." | ||||
| } | ||||
|   | ||||
| @@ -137,6 +137,11 @@ | ||||
|   "publisherRunBy": "由 {} 管理", | ||||
|   "fieldPublisherBelongToRealm": "所属领域", | ||||
|   "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域", | ||||
|   "writePost": "撰写", | ||||
|   "postTypeStory": "动态", | ||||
|   "postTypeArticle": "文章", | ||||
|   "postTypeQuestion": "问题", | ||||
|   "postTypeVideo": "视频", | ||||
|   "writePostTypeStory": "发动态", | ||||
|   "writePostTypeArticle": "写文章", | ||||
|   "writePostTypeQuestion": "提问题", | ||||
| @@ -201,6 +206,11 @@ | ||||
|     "other": "{} 条评论" | ||||
|   }, | ||||
|   "settingsAppearance": "外观", | ||||
|   "settingsCustomFonts": "自定义字体", | ||||
|   "settingsCustomFontsDescription": "设置应用程序使用的字体。", | ||||
|   "settingsCustomFontFamily": "应用字体", | ||||
|   "settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高", | ||||
|   "settingsCustomFontApplied": "自定义字体已经应用。", | ||||
|   "settingsDisplayLanguage": "显示语言", | ||||
|   "settingsDisplayLanguageDescription": "设置应用程序使用的语言", | ||||
|   "settingsDisplayLanguageSystem": "跟随系统", | ||||
| @@ -510,8 +520,13 @@ | ||||
|   "accountBirthday": "出生于 {}", | ||||
|   "accountBadge": "徽章", | ||||
|   "accountCheckInNoRecords": "暂无运势记录", | ||||
|   "badgeCompanyStaff": "索尔辛茨士大夫 · 员工", | ||||
|   "badgeCompanyStaff": "工作人员", | ||||
|   "badgeSiteMigration": "Solar Network 原住民", | ||||
|   "badgeCommunitySurvey": "调研参与者", | ||||
|   "badgeCommunityVerified": "认证用户", | ||||
|   "badgeCommunityContributor": "优秀社区贡献者", | ||||
|   "badgeSiteAnniversary": "周年纪念", | ||||
|   "badgeUserBirthday": "生日纪念", | ||||
|   "accountStatus": "状态", | ||||
|   "accountStatusOnline": "在线", | ||||
|   "accountStatusOffline": "离线", | ||||
| @@ -720,5 +735,37 @@ | ||||
|   "trayMenuMuteNotification": "静音通知", | ||||
|   "update": "更新", | ||||
|   "forceUpdate": "强制更新", | ||||
|   "forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。" | ||||
|   "forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。", | ||||
|   "runtimeLogs": "运行时日志", | ||||
|   "runtimeLogsOpen": "打开日志文件", | ||||
|   "runtimeLogsDescription": "显示运行时的日志记录。", | ||||
|   "signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。", | ||||
|   "cacheSize": "缓存资源大小", | ||||
|   "cacheDelete": "清除缓存", | ||||
|   "cacheDeleteDescription": "从磁盘中移除缓存的图片和其他资源,内容将从服务器重新下载。", | ||||
|   "cacheDeleted": "所有缓存已被清除。", | ||||
|   "userNoDescription": "这个人很懒,没有留下什么……", | ||||
|   "fieldTimeZone": "时区", | ||||
|   "fieldGender": "性别", | ||||
|   "fieldPronouns": "人称代词", | ||||
|   "fieldLocation": "位置", | ||||
|   "fieldLinks": "链接", | ||||
|   "fieldLinkName": "名称", | ||||
|   "fieldLinkUrl": "链接", | ||||
|   "screenAccountBadges": "徽章", | ||||
|   "accountBadges": "徽章", | ||||
|   "accountBadgesDescription": "查看并管理你的徽章。", | ||||
|   "badgeActivated": "已佩戴徽章 {}。", | ||||
|   "viewDetailedAttachment": "查看附件详情", | ||||
|   "screenKeyPairs": "密钥对", | ||||
|   "accountKeyPairs": "密钥对", | ||||
|   "accountKeyPairsDescription": "管理用于加密信息的密钥对。", | ||||
|   "enrollNewKeyPair": "新建密钥对", | ||||
|   "enrollNewKeyPairDescription": "生成一对新密钥对。", | ||||
|   "keyPairHasPrivateKey": "有私钥", | ||||
|   "decrypting": "解密中……", | ||||
|   "decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线", | ||||
|   "messageUnablePreview": "无法预览消息", | ||||
|   "messageUnablePreviewEncrypted": "无法预览加密消息", | ||||
|   "postViewInGlobalDescription": "不查看特定领域的帖子。" | ||||
| } | ||||
|   | ||||
| @@ -137,6 +137,11 @@ | ||||
|   "publisherRunBy": "由 {} 管理", | ||||
|   "fieldPublisherBelongToRealm": "所屬領域", | ||||
|   "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域", | ||||
|   "writePost": "撰寫", | ||||
|   "postTypeStory": "動態", | ||||
|   "postTypeArticle": "文章", | ||||
|   "postTypeQuestion": "問題", | ||||
|   "postTypeVideo": "視頻", | ||||
|   "writePostTypeStory": "發動態", | ||||
|   "writePostTypeArticle": "寫文章", | ||||
|   "writePostTypeQuestion": "提問題", | ||||
| @@ -201,6 +206,11 @@ | ||||
|     "other": "{} 條評論" | ||||
|   }, | ||||
|   "settingsAppearance": "外觀", | ||||
|   "settingsCustomFonts": "自定義字體", | ||||
|   "settingsCustomFontsDescription": "設置應用程序使用的字體。", | ||||
|   "settingsCustomFontFamily": "應用字體", | ||||
|   "settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高", | ||||
|   "settingsCustomFontApplied": "自定義字體已經應用。", | ||||
|   "settingsDisplayLanguage": "顯示語言", | ||||
|   "settingsDisplayLanguageDescription": "設置應用程序使用的語言", | ||||
|   "settingsDisplayLanguageSystem": "跟隨系統", | ||||
| @@ -510,8 +520,13 @@ | ||||
|   "accountBirthday": "出生於 {}", | ||||
|   "accountBadge": "徽章", | ||||
|   "accountCheckInNoRecords": "暫無運勢記錄", | ||||
|   "badgeCompanyStaff": "索爾辛茨士大夫 · 員工", | ||||
|   "badgeCompanyStaff": "工作人員", | ||||
|   "badgeSiteMigration": "Solar Network 原住民", | ||||
|   "badgeCommunitySurvey": "調研參與者", | ||||
|   "badgeCommunityVerified": "認證用户", | ||||
|   "badgeCommunityContributor": "優秀社區貢獻者", | ||||
|   "badgeSiteAnniversary": "週年紀念", | ||||
|   "badgeUserBirthday": "生日紀念", | ||||
|   "accountStatus": "狀態", | ||||
|   "accountStatusOnline": "在線", | ||||
|   "accountStatusOffline": "離線", | ||||
| @@ -720,5 +735,37 @@ | ||||
|   "trayMenuMuteNotification": "靜音通知", | ||||
|   "update": "更新", | ||||
|   "forceUpdate": "強制更新", | ||||
|   "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" | ||||
|   "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。", | ||||
|   "runtimeLogs": "運行時日誌", | ||||
|   "runtimeLogsOpen": "打開日誌文件", | ||||
|   "runtimeLogsDescription": "顯示運行時的日誌記錄。", | ||||
|   "signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。", | ||||
|   "cacheSize": "緩存資源大小", | ||||
|   "cacheDelete": "清除緩存", | ||||
|   "cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。", | ||||
|   "cacheDeleted": "所有緩存已被清除。", | ||||
|   "userNoDescription": "這個人很懶,沒有留下什麼……", | ||||
|   "fieldTimeZone": "時區", | ||||
|   "fieldGender": "性別", | ||||
|   "fieldPronouns": "人稱代詞", | ||||
|   "fieldLocation": "位置", | ||||
|   "fieldLinks": "鏈接", | ||||
|   "fieldLinkName": "名稱", | ||||
|   "fieldLinkUrl": "鏈接", | ||||
|   "screenAccountBadges": "徽章", | ||||
|   "accountBadges": "徽章", | ||||
|   "accountBadgesDescription": "查看並管理你的徽章。", | ||||
|   "badgeActivated": "已佩戴徽章 {}。", | ||||
|   "viewDetailedAttachment": "查看附件詳情", | ||||
|   "screenKeyPairs": "密鑰對", | ||||
|   "accountKeyPairs": "密鑰對", | ||||
|   "accountKeyPairsDescription": "管理用於加密信息的密鑰對。", | ||||
|   "enrollNewKeyPair": "新建密鑰對", | ||||
|   "enrollNewKeyPairDescription": "生成一對新密鑰對。", | ||||
|   "keyPairHasPrivateKey": "有私鑰", | ||||
|   "decrypting": "解密中……", | ||||
|   "decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線", | ||||
|   "messageUnablePreview": "無法預覽消息", | ||||
|   "messageUnablePreviewEncrypted": "無法預覽加密消息", | ||||
|   "postViewInGlobalDescription": "不查看特定領域的帖子。" | ||||
| } | ||||
|   | ||||
| @@ -137,6 +137,11 @@ | ||||
|   "publisherRunBy": "由 {} 管理", | ||||
|   "fieldPublisherBelongToRealm": "所屬領域", | ||||
|   "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域", | ||||
|   "writePost": "撰寫", | ||||
|   "postTypeStory": "動態", | ||||
|   "postTypeArticle": "文章", | ||||
|   "postTypeQuestion": "問題", | ||||
|   "postTypeVideo": "視頻", | ||||
|   "writePostTypeStory": "發動態", | ||||
|   "writePostTypeArticle": "寫文章", | ||||
|   "writePostTypeQuestion": "提問題", | ||||
| @@ -201,6 +206,11 @@ | ||||
|     "other": "{} 條評論" | ||||
|   }, | ||||
|   "settingsAppearance": "外觀", | ||||
|   "settingsCustomFonts": "自定義字體", | ||||
|   "settingsCustomFontsDescription": "設置應用程序使用的字體。", | ||||
|   "settingsCustomFontFamily": "應用字體", | ||||
|   "settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高", | ||||
|   "settingsCustomFontApplied": "自定義字體已經應用。", | ||||
|   "settingsDisplayLanguage": "顯示語言", | ||||
|   "settingsDisplayLanguageDescription": "設置應用程序使用的語言", | ||||
|   "settingsDisplayLanguageSystem": "跟隨系統", | ||||
| @@ -510,8 +520,13 @@ | ||||
|   "accountBirthday": "出生於 {}", | ||||
|   "accountBadge": "徽章", | ||||
|   "accountCheckInNoRecords": "暫無運勢記錄", | ||||
|   "badgeCompanyStaff": "索爾辛茨士大夫 · 員工", | ||||
|   "badgeCompanyStaff": "工作人員", | ||||
|   "badgeSiteMigration": "Solar Network 原住民", | ||||
|   "badgeCommunitySurvey": "調研參與者", | ||||
|   "badgeCommunityVerified": "認證用戶", | ||||
|   "badgeCommunityContributor": "優秀社區貢獻者", | ||||
|   "badgeSiteAnniversary": "週年紀念", | ||||
|   "badgeUserBirthday": "生日紀念", | ||||
|   "accountStatus": "狀態", | ||||
|   "accountStatusOnline": "在線", | ||||
|   "accountStatusOffline": "離線", | ||||
| @@ -720,5 +735,37 @@ | ||||
|   "trayMenuMuteNotification": "靜音通知", | ||||
|   "update": "更新", | ||||
|   "forceUpdate": "強制更新", | ||||
|   "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" | ||||
|   "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。", | ||||
|   "runtimeLogs": "運行時日誌", | ||||
|   "runtimeLogsOpen": "打開日誌文件", | ||||
|   "runtimeLogsDescription": "顯示運行時的日誌記錄。", | ||||
|   "signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。", | ||||
|   "cacheSize": "緩存資源大小", | ||||
|   "cacheDelete": "清除緩存", | ||||
|   "cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。", | ||||
|   "cacheDeleted": "所有緩存已被清除。", | ||||
|   "userNoDescription": "這個人很懶,沒有留下什麼……", | ||||
|   "fieldTimeZone": "時區", | ||||
|   "fieldGender": "性別", | ||||
|   "fieldPronouns": "人稱代詞", | ||||
|   "fieldLocation": "位置", | ||||
|   "fieldLinks": "鏈接", | ||||
|   "fieldLinkName": "名稱", | ||||
|   "fieldLinkUrl": "鏈接", | ||||
|   "screenAccountBadges": "徽章", | ||||
|   "accountBadges": "徽章", | ||||
|   "accountBadgesDescription": "查看並管理你的徽章。", | ||||
|   "badgeActivated": "已佩戴徽章 {}。", | ||||
|   "viewDetailedAttachment": "查看附件詳情", | ||||
|   "screenKeyPairs": "密鑰對", | ||||
|   "accountKeyPairs": "密鑰對", | ||||
|   "accountKeyPairsDescription": "管理用於加密信息的密鑰對。", | ||||
|   "enrollNewKeyPair": "新建密鑰對", | ||||
|   "enrollNewKeyPairDescription": "生成一對新密鑰對。", | ||||
|   "keyPairHasPrivateKey": "有私鑰", | ||||
|   "decrypting": "解密中……", | ||||
|   "decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線", | ||||
|   "messageUnablePreview": "無法預覽消息", | ||||
|   "messageUnablePreviewEncrypted": "無法預覽加密消息", | ||||
|   "postViewInGlobalDescription": "不查看特定領域的帖子。" | ||||
| } | ||||
|   | ||||
| @@ -4,4 +4,8 @@ targets: | ||||
|       json_serializable: | ||||
|         options: | ||||
|           explicit_to_json: true | ||||
|           field_rename: snake | ||||
|           field_rename: snake | ||||
|       drift_dev: | ||||
|         options: | ||||
|           databases: | ||||
|             my_database: lib/database/database.dart | ||||
							
								
								
									
										1
									
								
								drift_schemas/my_database/drift_schema_v1.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								drift_schemas/my_database/drift_schema_v1.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} | ||||
							
								
								
									
										1
									
								
								drift_schemas/my_database/drift_schema_v2.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								drift_schemas/my_database/drift_schema_v2.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}}]} | ||||
							
								
								
									
										1
									
								
								drift_schemas/my_database/drift_schema_v3.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								drift_schemas/my_database/drift_schema_v3.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -37,6 +37,8 @@ PODS: | ||||
|   - DKPhotoGallery/Resource (0.0.19): | ||||
|     - SDWebImage | ||||
|     - SwiftyGif | ||||
|   - fast_rsa (0.6.0): | ||||
|     - Flutter | ||||
|   - file_picker (0.0.1): | ||||
|     - DKImagePickerController/PhotoGallery | ||||
|     - Flutter | ||||
| @@ -52,14 +54,14 @@ PODS: | ||||
|   - Firebase/Messaging (11.8.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.8.0) | ||||
|   - firebase_analytics (11.4.3): | ||||
|   - firebase_analytics (11.4.4): | ||||
|     - Firebase/Analytics (= 11.8.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - firebase_core (3.12.0): | ||||
|   - firebase_core (3.12.1): | ||||
|     - Firebase/CoreOnly (= 11.8.0) | ||||
|     - Flutter | ||||
|   - firebase_messaging (15.2.3): | ||||
|   - firebase_messaging (15.2.4): | ||||
|     - Firebase/Messaging (= 11.8.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
| @@ -113,6 +115,8 @@ PODS: | ||||
|     - OrderedSet (~> 6.0.3) | ||||
|   - flutter_native_splash (2.4.3): | ||||
|     - Flutter | ||||
|   - flutter_timezone (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_udid (0.0.1): | ||||
|     - Flutter | ||||
|     - SAMKeychain | ||||
| @@ -122,6 +126,8 @@ PODS: | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - geolocator_apple (1.2.0): | ||||
|     - Flutter | ||||
|   - GoogleAppMeasurement (11.8.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.8.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
| @@ -235,7 +241,7 @@ PODS: | ||||
|   - sqlite3_flutter_libs (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|     - sqlite3 (~> 3.49.0) | ||||
|     - sqlite3 (~> 3.49.1) | ||||
|     - sqlite3/dbstatvtab | ||||
|     - sqlite3/fts5 | ||||
|     - sqlite3/perf-threadsafe | ||||
| @@ -258,6 +264,7 @@ DEPENDENCIES: | ||||
|   - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) | ||||
|   - croppy (from `.symlinks/plugins/croppy/ios`) | ||||
|   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) | ||||
|   - fast_rsa (from `.symlinks/plugins/fast_rsa/ios`) | ||||
|   - file_picker (from `.symlinks/plugins/file_picker/ios`) | ||||
|   - file_saver (from `.symlinks/plugins/file_saver/ios`) | ||||
|   - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) | ||||
| @@ -267,9 +274,11 @@ DEPENDENCIES: | ||||
|   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
|   - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) | ||||
|   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) | ||||
|   - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) | ||||
|   - gal (from `.symlinks/plugins/gal/darwin`) | ||||
|   - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) | ||||
|   - home_widget (from `.symlinks/plugins/home_widget/ios`) | ||||
|   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) | ||||
|   - in_app_review (from `.symlinks/plugins/in_app_review/ios`) | ||||
| @@ -325,6 +334,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/croppy/ios" | ||||
|   device_info_plus: | ||||
|     :path: ".symlinks/plugins/device_info_plus/ios" | ||||
|   fast_rsa: | ||||
|     :path: ".symlinks/plugins/fast_rsa/ios" | ||||
|   file_picker: | ||||
|     :path: ".symlinks/plugins/file_picker/ios" | ||||
|   file_saver: | ||||
| @@ -343,12 +354,16 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||
|   flutter_native_splash: | ||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||
|   flutter_timezone: | ||||
|     :path: ".symlinks/plugins/flutter_timezone/ios" | ||||
|   flutter_udid: | ||||
|     :path: ".symlinks/plugins/flutter_udid/ios" | ||||
|   flutter_webrtc: | ||||
|     :path: ".symlinks/plugins/flutter_webrtc/ios" | ||||
|   gal: | ||||
|     :path: ".symlinks/plugins/gal/darwin" | ||||
|   geolocator_apple: | ||||
|     :path: ".symlinks/plugins/geolocator_apple/ios" | ||||
|   home_widget: | ||||
|     :path: ".symlinks/plugins/home_widget/ios" | ||||
|   image_picker_ios: | ||||
| @@ -401,12 +416,13 @@ SPEC CHECKSUMS: | ||||
|   device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 | ||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   fast_rsa: dc48fb05f26bb108863de122b2a9f5554e8e2591 | ||||
|   file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 | ||||
|   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 | ||||
|   Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf | ||||
|   firebase_analytics: 7ec1166af61987fa968766eb11587c562a5650ee | ||||
|   firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682 | ||||
|   firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d | ||||
|   firebase_analytics: e3b6782e70e32b7fa18f7cd233e3201975dd86aa | ||||
|   firebase_core: ac395f994af4e28f6a38b59e05a88ca57abeb874 | ||||
|   firebase_messaging: 7e223f4ee7ca053bf4ce43748e84a6d774ec9728 | ||||
|   FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b | ||||
|   FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d | ||||
|   FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 | ||||
| @@ -416,9 +432,11 @@ SPEC CHECKSUMS: | ||||
|   flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc | ||||
|   flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 | ||||
|   flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 | ||||
|   flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 | ||||
|   flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab | ||||
|   flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1 | ||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||
|   geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1 | ||||
|   GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||
| @@ -445,7 +463,7 @@ SPEC CHECKSUMS: | ||||
|   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 | ||||
|   sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d | ||||
|   sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 | ||||
|   sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa | ||||
|   sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db | ||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||
|   url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe | ||||
|   video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| @@ -8,7 +7,10 @@ import 'package:drift/drift.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/database/database.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/database.dart'; | ||||
| import 'package:surface/providers/keypair.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| @@ -25,6 +27,8 @@ class ChatMessageController extends ChangeNotifier { | ||||
|   late final WebSocketProvider _ws; | ||||
|   late final SnAttachmentProvider _attach; | ||||
|   late final DatabaseProvider _dt; | ||||
|   late final ChatChannelProvider _ct; | ||||
|   late final KeyPairProvider _kp; | ||||
|  | ||||
|   StreamSubscription? _wsSubscription; | ||||
|  | ||||
| @@ -33,11 +37,14 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     _ud = context.read<UserDirectoryProvider>(); | ||||
|     _ws = context.read<WebSocketProvider>(); | ||||
|     _attach = context.read<SnAttachmentProvider>(); | ||||
|     _ct = context.read<ChatChannelProvider>(); | ||||
|     _dt = context.read<DatabaseProvider>(); | ||||
|     _kp = context.read<KeyPairProvider>(); | ||||
|   } | ||||
|  | ||||
|   bool isPending = true; | ||||
|   bool isLoading = false; | ||||
|   bool isAggressiveLoading = false; | ||||
|  | ||||
|   int? messageTotal; | ||||
|  | ||||
| @@ -61,10 +68,7 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     channel = chan; | ||||
|  | ||||
|     // Fetch channel profile | ||||
|     final resp = await _sn.client.get( | ||||
|       '/cgi/im/channels/${chan.keyPath}/me', | ||||
|     ); | ||||
|     profile = SnChannelMember.fromJson(resp.data); | ||||
|     profile = await _ct.getChannelProfile(channel!); | ||||
|  | ||||
|     _wsSubscription = _ws.pk.stream.listen((event) { | ||||
|       switch (event.method) { | ||||
| @@ -183,6 +187,7 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     } else { | ||||
|       messages.insert(0, message); | ||||
|     } | ||||
|     notifyListeners(); | ||||
|     await _applyMessage(message); | ||||
|     notifyListeners(); | ||||
|  | ||||
| @@ -243,6 +248,24 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<Map<String, dynamic>> _encodeMessageBody( | ||||
|     String text, | ||||
|     bool isEncrypted, | ||||
|   ) async { | ||||
|     if (!isEncrypted || _kp.activeKp == null) { | ||||
|       return { | ||||
|         'text': text, | ||||
|         'algorithm': 'plain', | ||||
|       }; | ||||
|     } else { | ||||
|       return { | ||||
|         'text': await _kp.encryptText(text), | ||||
|         'algorithm': 'rsa', | ||||
|         'keypair_id': _kp.activeKp!.id, | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> sendMessage( | ||||
|     String type, | ||||
|     String content, { | ||||
| @@ -250,13 +273,13 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     int? relatedId, | ||||
|     List<String>? attachments, | ||||
|     SnChatMessage? editingMessage, | ||||
|     bool isEncrypted = false, | ||||
|   }) async { | ||||
|     if (channel == null) return; | ||||
|     const uuid = Uuid(); | ||||
|     final nonce = uuid.v4(); | ||||
|     final body = { | ||||
|       'text': content, | ||||
|       'algorithm': 'plain', | ||||
|       ...(await _encodeMessageBody(content, isEncrypted)), | ||||
|       if (quoteId != null) 'quote_event': quoteId, | ||||
|       if (relatedId != null) 'related_event': relatedId, | ||||
|       if (attachments != null && attachments.isNotEmpty) | ||||
| @@ -264,23 +287,26 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     }; | ||||
|  | ||||
|     // Mock the message locally | ||||
|     final createdAt = DateTime.now(); | ||||
|     final message = SnChatMessage( | ||||
|       id: 0, | ||||
|       createdAt: createdAt, | ||||
|       updatedAt: createdAt, | ||||
|       deletedAt: null, | ||||
|       uuid: nonce, | ||||
|       body: body, | ||||
|       type: type, | ||||
|       channel: channel!, | ||||
|       channelId: channel!.id, | ||||
|       sender: profile!, | ||||
|       senderId: profile!.id, | ||||
|       quoteEventId: quoteId, | ||||
|       relatedEventId: relatedId, | ||||
|     ); | ||||
|     _addUnconfirmedMessage(message); | ||||
|     // Do not mock the editing message | ||||
|     if (editingMessage == null) { | ||||
|       final createdAt = DateTime.now(); | ||||
|       final message = SnChatMessage( | ||||
|         id: 0, | ||||
|         createdAt: createdAt, | ||||
|         updatedAt: createdAt, | ||||
|         deletedAt: null, | ||||
|         uuid: nonce, | ||||
|         body: body, | ||||
|         type: type, | ||||
|         channel: channel!, | ||||
|         channelId: channel!.id, | ||||
|         sender: profile!, | ||||
|         senderId: profile!.id, | ||||
|         quoteEventId: quoteId, | ||||
|         relatedEventId: relatedId, | ||||
|       ); | ||||
|       _addUnconfirmedMessage(message); | ||||
|     } | ||||
|  | ||||
|     // Send to server | ||||
|     try { | ||||
| @@ -320,7 +346,7 @@ class ChatMessageController extends ChangeNotifier { | ||||
|   /// Check the local storage is up to date with the server. | ||||
|   /// If the local storage is not up to date, it will be updated. | ||||
|   Future<void> checkUpdate() async { | ||||
|     isLoading = true; | ||||
|     isAggressiveLoading = true; | ||||
|     notifyListeners(); | ||||
|  | ||||
|     final mostRecentMessage = await (_dt.db.snLocalChatMessage.select() | ||||
| @@ -334,6 +360,7 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     if (mostRecentMessage == null) { | ||||
|       // Initial load | ||||
|       await loadMessages(take: 20); | ||||
|       isAggressiveLoading = false; | ||||
|       isCheckedUpdate = true; | ||||
|       return; | ||||
|     } | ||||
| @@ -351,13 +378,19 @@ class ChatMessageController extends ChangeNotifier { | ||||
|       final countToFetch = math.min(resp.data['count'] as int, 100); | ||||
|  | ||||
|       for (int idx = 0; idx < countToFetch; idx += kSingleBatchLoadLimit) { | ||||
|         await getMessages(kSingleBatchLoadLimit, idx, forceRemote: true); | ||||
|         final out = await getMessages( | ||||
|           kSingleBatchLoadLimit, | ||||
|           idx, | ||||
|           forceRemote: true, | ||||
|         ); | ||||
|         messages.insertAll(0, out); | ||||
|         notifyListeners(); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       rethrow; | ||||
|     } finally { | ||||
|       await loadMessages(); | ||||
|       isLoading = false; | ||||
|       isAggressiveLoading = false; | ||||
|  | ||||
|       isCheckedUpdate = true; | ||||
|       _saveMessageToLocal(incomeStrandedQueue).then((_) { | ||||
| @@ -532,7 +565,7 @@ class ChatMessageController extends ChangeNotifier { | ||||
|         }, | ||||
|       ).toJson(), | ||||
|     )); | ||||
|     log('[Messaging] Send read event request: $_readEventAnchor'); | ||||
|     logging.debug('[Messaging] Send read event request: $_readEventAnchor'); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -71,7 +71,8 @@ class PostWriteMedia { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   PostWriteMedia.fromBytes(this.raw, this.name, this.type, {this.attachment, this.file}); | ||||
|   PostWriteMedia.fromBytes(this.raw, this.name, this.type, | ||||
|       {this.attachment, this.file}); | ||||
|  | ||||
|   bool get isEmpty => attachment == null && file == null && raw == null; | ||||
|  | ||||
| @@ -105,7 +106,8 @@ class PostWriteMedia { | ||||
|   }) { | ||||
|     if (attachment != null) { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid)); | ||||
|       final ImageProvider provider = | ||||
|           UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid)); | ||||
|       if (width != null && height != null && !kIsWeb) { | ||||
|         return ResizeImage( | ||||
|           provider, | ||||
| @@ -116,7 +118,8 @@ class PostWriteMedia { | ||||
|       } | ||||
|       return provider; | ||||
|     } else if (file != null) { | ||||
|       final ImageProvider provider = kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path)); | ||||
|       final ImageProvider provider = | ||||
|           kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path)); | ||||
|       if (width != null && height != null) { | ||||
|         return ResizeImage( | ||||
|           provider, | ||||
| @@ -159,11 +162,14 @@ class PostWriteController extends ChangeNotifier { | ||||
|   final TextEditingController aliasController = TextEditingController(); | ||||
|   final TextEditingController rewardController = TextEditingController(); | ||||
|  | ||||
|   ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration( | ||||
|   ContentInsertionConfiguration get contentInsertionConfiguration => | ||||
|       ContentInsertionConfiguration( | ||||
|         onContentInserted: (KeyboardInsertedContent content) { | ||||
|           if (content.hasData) { | ||||
|             addAttachments( | ||||
|                 [PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]); | ||||
|             addAttachments([ | ||||
|               PostWriteMedia.fromBytes(content.data!, | ||||
|                   'attachmentInsertedImage'.tr(), SnMediaType.image) | ||||
|             ]); | ||||
|           } | ||||
|         }, | ||||
|       ); | ||||
| @@ -193,7 +199,8 @@ class PostWriteController extends ChangeNotifier { | ||||
|  | ||||
|   String get description => descriptionController.text; | ||||
|  | ||||
|   bool get isRelatedNull => ![editingPost, repostingPost, replyingPost].any((ele) => ele != null); | ||||
|   bool get isRelatedNull => | ||||
|       ![editingPost, repostingPost, replyingPost].any((ele) => ele != null); | ||||
|  | ||||
|   bool isLoading = false, isBusy = false; | ||||
|   double? progress; | ||||
| @@ -237,14 +244,18 @@ class PostWriteController extends ChangeNotifier { | ||||
|         publishedAt = post.publishedAt; | ||||
|         publishedUntil = post.publishedUntil; | ||||
|         visibleUsers = List.from(post.visibleUsersList ?? [], growable: true); | ||||
|         invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true); | ||||
|         invisibleUsers = | ||||
|             List.from(post.invisibleUsersList ?? [], growable: true); | ||||
|         visibility = post.visibility; | ||||
|         tags = List.from(post.tags.map((ele) => ele.alias), growable: true); | ||||
|         categories = List.from(post.categories.map((ele) => ele.alias), growable: true); | ||||
|         attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); | ||||
|         categories = | ||||
|             List.from(post.categories.map((ele) => ele.alias), growable: true); | ||||
|         attachments.addAll( | ||||
|             post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); | ||||
|         poll = post.preload?.poll; | ||||
|  | ||||
|         if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { | ||||
|         if (post.preload?.thumbnail != null && | ||||
|             (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { | ||||
|           thumbnail = PostWriteMedia(post.preload!.thumbnail); | ||||
|         } | ||||
|         if (post.preload?.realm != null) { | ||||
| @@ -272,7 +283,8 @@ class PostWriteController extends ChangeNotifier { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media, | ||||
|   Future<SnAttachment> _uploadAttachment( | ||||
|       BuildContext context, PostWriteMedia media, | ||||
|       {bool isCompressed = false}) async { | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|  | ||||
| @@ -281,7 +293,9 @@ class PostWriteController extends ChangeNotifier { | ||||
|       media.name, | ||||
|       'interactive', | ||||
|       null, | ||||
|       mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, | ||||
|       mimetype: media.raw != null && media.type == SnMediaType.image | ||||
|           ? 'image/png' | ||||
|           : null, | ||||
|     ); | ||||
|  | ||||
|     var item = await attach.chunkedUploadParts( | ||||
| @@ -297,9 +311,11 @@ class PostWriteController extends ChangeNotifier { | ||||
|  | ||||
|     if (media.type == SnMediaType.video && !isCompressed && context.mounted) { | ||||
|       try { | ||||
|         final compressedAttachment = await _tryCompressVideoCopy(context, media); | ||||
|         final compressedAttachment = | ||||
|             await _tryCompressVideoCopy(context, media); | ||||
|         if (compressedAttachment != null) { | ||||
|           item = await attach.updateOne(item, compressedId: compressedAttachment.id); | ||||
|           item = await attach.updateOne(item, | ||||
|               compressedId: compressedAttachment.id); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         if (context.mounted) context.showErrorDialog(err); | ||||
| @@ -309,8 +325,10 @@ class PostWriteController extends ChangeNotifier { | ||||
|     return item; | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async { | ||||
|     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null; | ||||
|   Future<SnAttachment?> _tryCompressVideoCopy( | ||||
|       BuildContext context, PostWriteMedia media) async { | ||||
|     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) | ||||
|       return null; | ||||
|     if (media.type != SnMediaType.video) return null; | ||||
|     if (media.file == null) return null; | ||||
|     if (VideoCompress.isCompressing) return null; | ||||
| @@ -334,7 +352,8 @@ class PostWriteController extends ChangeNotifier { | ||||
|     if (!context.mounted) return null; | ||||
|  | ||||
|     final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!)); | ||||
|     final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true); | ||||
|     final compressedAttachment = | ||||
|         await _uploadAttachment(context, compressedMedia, isCompressed: true); | ||||
|  | ||||
|     return compressedAttachment; | ||||
|   } | ||||
| @@ -370,18 +389,25 @@ class PostWriteController extends ChangeNotifier { | ||||
|           'content': contentController.text, | ||||
|           if (aliasController.text.isNotEmpty) 'alias': aliasController.text, | ||||
|           if (titleController.text.isNotEmpty) 'title': titleController.text, | ||||
|           if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, | ||||
|           if (descriptionController.text.isNotEmpty) | ||||
|             'description': descriptionController.text, | ||||
|           if (rewardController.text.isNotEmpty) 'reward': rewardController.text, | ||||
|           if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(), | ||||
|           'attachments': | ||||
|               attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true), | ||||
|           if (thumbnail != null && thumbnail!.attachment != null) | ||||
|             'thumbnail': thumbnail!.attachment!.toJson(), | ||||
|           'attachments': attachments | ||||
|               .where((e) => e.attachment != null) | ||||
|               .map((e) => e.attachment!.toJson()) | ||||
|               .toList(growable: true), | ||||
|           'tags': tags.map((ele) => {'alias': ele}).toList(growable: true), | ||||
|           'categories': categories.map((ele) => {'alias': ele}).toList(growable: true), | ||||
|           'categories': | ||||
|               categories.map((ele) => {'alias': ele}).toList(growable: true), | ||||
|           'visibility': visibility, | ||||
|           'visible_users_list': visibleUsers, | ||||
|           'invisible_users_list': invisibleUsers, | ||||
|           if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (publishedAt != null) | ||||
|             'published_at': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (publishedUntil != null) | ||||
|             'published_until': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (replyingPost != null) 'reply_to': replyingPost!.toJson(), | ||||
|           if (repostingPost != null) 'repost_to': repostingPost!.toJson(), | ||||
|           if (poll != null) 'poll': poll!.toJson(), | ||||
| @@ -391,6 +417,12 @@ class PostWriteController extends ChangeNotifier { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   bool get isNotEmpty => | ||||
|       title.isNotEmpty || | ||||
|       description.isNotEmpty || | ||||
|       contentController.text.isNotEmpty || | ||||
|       attachments.isNotEmpty; | ||||
|  | ||||
|   bool temporaryRestored = false; | ||||
|  | ||||
|   void _temporaryLoad() { | ||||
| @@ -403,18 +435,24 @@ class PostWriteController extends ChangeNotifier { | ||||
|       titleController.text = data['title'] ?? ''; | ||||
|       descriptionController.text = data['description'] ?? ''; | ||||
|       rewardController.text = data['reward']?.toString() ?? ''; | ||||
|       if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail'])); | ||||
|       attachments | ||||
|           .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>()); | ||||
|       if (data['thumbnail'] != null) | ||||
|         thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail'])); | ||||
|       attachments.addAll(data['attachments'] | ||||
|           .map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))) | ||||
|           .cast<PostWriteMedia>()); | ||||
|       tags = List.from(data['tags'].map((ele) => ele['alias'])); | ||||
|       categories = List.from(data['categories'].map((ele) => ele['alias'])); | ||||
|       visibility = data['visibility']; | ||||
|       visibleUsers = List.from(data['visible_users_list'] ?? []); | ||||
|       invisibleUsers = List.from(data['invisible_users_list'] ?? []); | ||||
|       if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal(); | ||||
|       if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal(); | ||||
|       replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null; | ||||
|       repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null; | ||||
|       if (data['published_at'] != null) | ||||
|         publishedAt = DateTime.tryParse(data['published_at'])?.toLocal(); | ||||
|       if (data['published_until'] != null) | ||||
|         publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal(); | ||||
|       replyingPost = | ||||
|           data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null; | ||||
|       repostingPost = | ||||
|           data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null; | ||||
|       poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null; | ||||
|       realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null; | ||||
|       temporaryRestored = true; | ||||
| @@ -463,7 +501,9 @@ class PostWriteController extends ChangeNotifier { | ||||
|           media.name, | ||||
|           'interactive', | ||||
|           null, | ||||
|           mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, | ||||
|           mimetype: media.raw != null && media.type == SnMediaType.image | ||||
|               ? 'image/png' | ||||
|               : null, | ||||
|         ); | ||||
|  | ||||
|         var item = await attach.chunkedUploadParts( | ||||
| @@ -472,16 +512,20 @@ class PostWriteController extends ChangeNotifier { | ||||
|           place.$2, | ||||
|           onProgress: (value) { | ||||
|             // Calculate overall progress for attachments | ||||
|             progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value); | ||||
|             progress = math.max( | ||||
|                 ((i + value) / attachments.length) * kAttachmentProgressWeight, | ||||
|                 value); | ||||
|             notifyListeners(); | ||||
|           }, | ||||
|         ); | ||||
|  | ||||
|         try { | ||||
|           if (context.mounted) { | ||||
|             final compressedAttachment = await _tryCompressVideoCopy(context, media); | ||||
|             final compressedAttachment = | ||||
|                 await _tryCompressVideoCopy(context, media); | ||||
|             if (compressedAttachment != null) { | ||||
|               item = await attach.updateOne(item, compressedId: compressedAttachment.id); | ||||
|               item = await attach.updateOne(item, | ||||
|                   compressedId: compressedAttachment.id); | ||||
|             } | ||||
|           } | ||||
|         } catch (err) { | ||||
| @@ -518,16 +562,23 @@ class PostWriteController extends ChangeNotifier { | ||||
|           'content': contentController.text, | ||||
|           if (aliasController.text.isNotEmpty) 'alias': aliasController.text, | ||||
|           if (titleController.text.isNotEmpty) 'title': titleController.text, | ||||
|           if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, | ||||
|           if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid, | ||||
|           'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), | ||||
|           if (descriptionController.text.isNotEmpty) | ||||
|             'description': descriptionController.text, | ||||
|           if (thumbnail != null && thumbnail!.attachment != null) | ||||
|             'thumbnail': thumbnail!.attachment!.rid, | ||||
|           'attachments': attachments | ||||
|               .where((e) => e.attachment != null) | ||||
|               .map((e) => e.attachment!.rid) | ||||
|               .toList(), | ||||
|           'tags': tags.map((ele) => {'alias': ele}).toList(), | ||||
|           'categories': categories.map((ele) => {'alias': ele}).toList(), | ||||
|           'visibility': visibility, | ||||
|           'visible_users_list': visibleUsers, | ||||
|           'invisible_users_list': invisibleUsers, | ||||
|           if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (publishedAt != null) | ||||
|             'published_at': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (publishedUntil != null) | ||||
|             'published_until': publishedAt!.toUtc().toIso8601String(), | ||||
|           if (replyingPost != null) 'reply_to': replyingPost!.id, | ||||
|           if (repostingPost != null) 'repost_to': repostingPost!.id, | ||||
|           if (reward != null) 'reward': reward, | ||||
| @@ -536,11 +587,14 @@ class PostWriteController extends ChangeNotifier { | ||||
|           if (realm != null) 'realm': realm!.id, | ||||
|         }, | ||||
|         onSendProgress: (count, total) { | ||||
|           progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2); | ||||
|           progress = | ||||
|               baseProgressVal + (count / total) * (kPostingProgressWeight / 2); | ||||
|           notifyListeners(); | ||||
|         }, | ||||
|         onReceiveProgress: (count, total) { | ||||
|           progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2); | ||||
|           progress = baseProgressVal + | ||||
|               (kPostingProgressWeight / 2) + | ||||
|               (count / total) * (kPostingProgressWeight / 2); | ||||
|           notifyListeners(); | ||||
|         }, | ||||
|         options: Options( | ||||
| @@ -683,7 +737,8 @@ class PostWriteController extends ChangeNotifier { | ||||
|     repostingPost = null; | ||||
|     mode = kTitleMap.keys.first; | ||||
|     temporaryRestored = false; | ||||
|     SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey)); | ||||
|     SharedPreferences.getInstance() | ||||
|         .then((prefs) => prefs.remove(kTemporaryStorageKey)); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										42
									
								
								lib/database/account.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								lib/database/account.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
|  | ||||
| class SnAccountConverter extends TypeConverter<SnAccount, String> | ||||
|     with JsonTypeConverter2<SnAccount, String, Map<String, Object?>> { | ||||
|   const SnAccountConverter(); | ||||
|  | ||||
|   @override | ||||
|   SnAccount fromSql(String fromDb) { | ||||
|     return fromJson(jsonDecode(fromDb) as Map<String, dynamic>); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toSql(SnAccount value) { | ||||
|     return jsonEncode(toJson(value)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   SnAccount fromJson(Map<String, Object?> json) { | ||||
|     return SnAccount.fromJson(json); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Map<String, Object?> toJson(SnAccount value) { | ||||
|     return value.toJson(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @TableIndex(name: 'idx_account_name', columns: {#name}) | ||||
| class SnLocalAccount extends Table { | ||||
|   IntColumn get id => integer().autoIncrement()(); | ||||
|  | ||||
|   TextColumn get name => text()(); | ||||
|  | ||||
|   TextColumn get content => text().map(const SnAccountConverter())(); | ||||
|  | ||||
|   DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); | ||||
|  | ||||
|   DateTimeColumn get cacheExpiredAt => dateTime()(); | ||||
| } | ||||
							
								
								
									
										47
									
								
								lib/database/attachment.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/database/attachment.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
|  | ||||
| class SnAttachmentConverter extends TypeConverter<SnAttachment, String> | ||||
|     with JsonTypeConverter2<SnAttachment, String, Map<String, Object?>> { | ||||
|   const SnAttachmentConverter(); | ||||
|  | ||||
|   @override | ||||
|   SnAttachment fromSql(String fromDb) { | ||||
|     return fromJson(jsonDecode(fromDb) as Map<String, dynamic>); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toSql(SnAttachment value) { | ||||
|     return jsonEncode(toJson(value)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   SnAttachment fromJson(Map<String, Object?> json) { | ||||
|     return SnAttachment.fromJson(json); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Map<String, Object?> toJson(SnAttachment value) { | ||||
|     return value.toJson(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @TableIndex(name: 'idx_attachment_rid', columns: {#rid}) | ||||
| @TableIndex(name: 'idx_attachment_account', columns: {#accountId}) | ||||
| class SnLocalAttachment extends Table { | ||||
|   IntColumn get id => integer().autoIncrement()(); | ||||
|  | ||||
|   TextColumn get rid => text().unique()(); | ||||
|  | ||||
|   TextColumn get uuid => text().unique()(); | ||||
|  | ||||
|   TextColumn get content => text().map(const SnAttachmentConverter())(); | ||||
|  | ||||
|   IntColumn get accountId => integer()(); | ||||
|  | ||||
|   DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); | ||||
|  | ||||
|   DateTimeColumn get cacheExpiredAt => dateTime()(); | ||||
| } | ||||
| @@ -28,6 +28,7 @@ class SnChannelConverter extends TypeConverter<SnChannel, String> | ||||
|   } | ||||
| } | ||||
|  | ||||
| @TableIndex(name: 'idx_channel_alias', columns: {#alias}) | ||||
| class SnLocalChatChannel extends Table { | ||||
|   IntColumn get id => integer().autoIncrement()(); | ||||
|  | ||||
| @@ -63,12 +64,54 @@ class SnMessageConverter extends TypeConverter<SnChatMessage, String> | ||||
|   } | ||||
| } | ||||
|  | ||||
| @TableIndex(name: 'idx_chat_channel', columns: {#channelId}) | ||||
| class SnLocalChatMessage extends Table { | ||||
|   IntColumn get id => integer().autoIncrement()(); | ||||
|  | ||||
|   IntColumn get channelId => integer()(); | ||||
|  | ||||
|   IntColumn get senderId => integer().nullable()(); | ||||
|  | ||||
|   TextColumn get content => text().map(const SnMessageConverter())(); | ||||
|  | ||||
|   DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); | ||||
| } | ||||
|  | ||||
| class SnChannelMemberConverter extends TypeConverter<SnChannelMember, String> | ||||
|     with JsonTypeConverter2<SnChannelMember, String, Map<String, Object?>> { | ||||
|   const SnChannelMemberConverter(); | ||||
|  | ||||
|   @override | ||||
|   SnChannelMember fromSql(String fromDb) { | ||||
|     return fromJson(jsonDecode(fromDb) as Map<String, dynamic>); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toSql(SnChannelMember value) { | ||||
|     return jsonEncode(toJson(value)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   SnChannelMember fromJson(Map<String, Object?> json) { | ||||
|     return SnChannelMember.fromJson(json); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Map<String, Object?> toJson(SnChannelMember value) { | ||||
|     return value.toJson(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class SnLocalChannelMember extends Table { | ||||
|   IntColumn get id => integer().autoIncrement()(); | ||||
|  | ||||
|   IntColumn get channelId => integer()(); | ||||
|  | ||||
|   IntColumn get accountId => integer()(); | ||||
|  | ||||
|   TextColumn get content => text().map(SnChannelMemberConverter())(); | ||||
|  | ||||
|   DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); | ||||
|  | ||||
|   DateTimeColumn get cacheExpiredAt => dateTime()(); | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,33 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:drift_flutter/drift_flutter.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:surface/database/account.dart'; | ||||
| import 'package:surface/database/attachment.dart'; | ||||
| import 'package:surface/database/chat.dart'; | ||||
| import 'package:surface/database/database.steps.dart'; | ||||
| import 'package:surface/database/keypair.dart'; | ||||
| import 'package:surface/database/sticker.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
|  | ||||
| part 'database.g.dart'; | ||||
|  | ||||
| @DriftDatabase(tables: [SnLocalChatChannel, SnLocalChatMessage]) | ||||
| @DriftDatabase(tables: [ | ||||
|   SnLocalChatChannel, | ||||
|   SnLocalChatMessage, | ||||
|   SnLocalChannelMember, | ||||
|   SnLocalKeyPair, | ||||
|   SnLocalAccount, | ||||
|   SnLocalAttachment, | ||||
|   SnLocalSticker, | ||||
|   SnLocalStickerPack, | ||||
| ]) | ||||
| class AppDatabase extends _$AppDatabase { | ||||
|   AppDatabase() : super(_openConnection()); | ||||
|   AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection()); | ||||
|  | ||||
|   @override | ||||
|   int get schemaVersion => 1; | ||||
|   int get schemaVersion => 3; | ||||
|  | ||||
|   static QueryExecutor _openConnection() { | ||||
|     return driftDatabase( | ||||
| @@ -25,4 +41,15 @@ class AppDatabase extends _$AppDatabase { | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MigrationStrategy get migration { | ||||
|     return MigrationStrategy( | ||||
|       onUpgrade: stepByStep(from1To2: (m, schema) async { | ||||
|         // Nothing else to do here | ||||
|       }, from2To3: (m, schema) async { | ||||
|         // Nothing else to do here, too | ||||
|       }), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										445
									
								
								lib/database/database.steps.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								lib/database/database.steps.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,445 @@ | ||||
| // dart format width=80 | ||||
| import 'package:drift/internal/versioned_schema.dart' as i0; | ||||
| import 'package:drift/drift.dart' as i1; | ||||
| import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import | ||||
|  | ||||
| // GENERATED BY drift_dev, DO NOT MODIFY. | ||||
| final class Schema2 extends i0.VersionedSchema { | ||||
|   Schema2({required super.database}) : super(version: 2); | ||||
|   @override | ||||
|   late final List<i1.DatabaseSchemaEntity> entities = [ | ||||
|     snLocalChatChannel, | ||||
|     snLocalChatMessage, | ||||
|     snLocalKeyPair, | ||||
|   ]; | ||||
|   late final Shape0 snLocalChatChannel = Shape0( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_chat_channel', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_1, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape1 snLocalChatMessage = Shape1( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_chat_message', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_4, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape2 snLocalKeyPair = Shape2( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_key_pair', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [ | ||||
|           'PRIMARY KEY(id)', | ||||
|         ], | ||||
|         columns: [ | ||||
|           _column_5, | ||||
|           _column_6, | ||||
|           _column_7, | ||||
|           _column_8, | ||||
|           _column_9, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
| } | ||||
|  | ||||
| class Shape0 extends i0.VersionedTable { | ||||
|   Shape0({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get alias => | ||||
|       columnsByName['alias']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<int> _column_0(String aliasedName) => | ||||
|     i1.GeneratedColumn<int>('id', aliasedName, false, | ||||
|         hasAutoIncrement: true, | ||||
|         type: i1.DriftSqlType.int, | ||||
|         defaultConstraints: | ||||
|             i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); | ||||
| i1.GeneratedColumn<String> _column_1(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('alias', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string); | ||||
| i1.GeneratedColumn<String> _column_2(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('content', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string); | ||||
| i1.GeneratedColumn<DateTime> _column_3(String aliasedName) => | ||||
|     i1.GeneratedColumn<DateTime>('created_at', aliasedName, false, | ||||
|         type: i1.DriftSqlType.dateTime, | ||||
|         defaultValue: const CustomExpression( | ||||
|             'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); | ||||
|  | ||||
| class Shape1 extends i0.VersionedTable { | ||||
|   Shape1({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<int> get channelId => | ||||
|       columnsByName['channel_id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<int> _column_4(String aliasedName) => | ||||
|     i1.GeneratedColumn<int>('channel_id', aliasedName, false, | ||||
|         type: i1.DriftSqlType.int); | ||||
|  | ||||
| class Shape2 extends i0.VersionedTable { | ||||
|   Shape2({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<String> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<int> get accountId => | ||||
|       columnsByName['account_id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get publicKey => | ||||
|       columnsByName['public_key']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<String> get privateKey => | ||||
|       columnsByName['private_key']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<bool> get isActive => | ||||
|       columnsByName['is_active']! as i1.GeneratedColumn<bool>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<String> _column_5(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('id', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string); | ||||
| i1.GeneratedColumn<int> _column_6(String aliasedName) => | ||||
|     i1.GeneratedColumn<int>('account_id', aliasedName, false, | ||||
|         type: i1.DriftSqlType.int); | ||||
| i1.GeneratedColumn<String> _column_7(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('public_key', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string); | ||||
| i1.GeneratedColumn<String> _column_8(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('private_key', aliasedName, true, | ||||
|         type: i1.DriftSqlType.string); | ||||
| i1.GeneratedColumn<bool> _column_9(String aliasedName) => | ||||
|     i1.GeneratedColumn<bool>('is_active', aliasedName, false, | ||||
|         type: i1.DriftSqlType.bool, | ||||
|         defaultConstraints: i1.GeneratedColumn.constraintIsAlways( | ||||
|             'CHECK ("is_active" IN (0, 1))'), | ||||
|         defaultValue: const CustomExpression('0')); | ||||
|  | ||||
| final class Schema3 extends i0.VersionedSchema { | ||||
|   Schema3({required super.database}) : super(version: 3); | ||||
|   @override | ||||
|   late final List<i1.DatabaseSchemaEntity> entities = [ | ||||
|     snLocalChatChannel, | ||||
|     snLocalChatMessage, | ||||
|     snLocalChannelMember, | ||||
|     snLocalKeyPair, | ||||
|     snLocalAccount, | ||||
|     snLocalAttachment, | ||||
|     snLocalSticker, | ||||
|     snLocalStickerPack, | ||||
|     idxChannelAlias, | ||||
|     idxChatChannel, | ||||
|     idxAccountName, | ||||
|     idxAttachmentRid, | ||||
|     idxAttachmentAccount, | ||||
|   ]; | ||||
|   late final Shape0 snLocalChatChannel = Shape0( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_chat_channel', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_1, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape3 snLocalChatMessage = Shape3( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_chat_message', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_4, | ||||
|           _column_10, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape4 snLocalChannelMember = Shape4( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_channel_member', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_4, | ||||
|           _column_6, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|           _column_11, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape2 snLocalKeyPair = Shape2( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_key_pair', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [ | ||||
|           'PRIMARY KEY(id)', | ||||
|         ], | ||||
|         columns: [ | ||||
|           _column_5, | ||||
|           _column_6, | ||||
|           _column_7, | ||||
|           _column_8, | ||||
|           _column_9, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape5 snLocalAccount = Shape5( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_account', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_12, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|           _column_11, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape6 snLocalAttachment = Shape6( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_attachment', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_13, | ||||
|           _column_14, | ||||
|           _column_2, | ||||
|           _column_6, | ||||
|           _column_3, | ||||
|           _column_11, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape7 snLocalSticker = Shape7( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_sticker', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_1, | ||||
|           _column_15, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   late final Shape8 snLocalStickerPack = Shape8( | ||||
|       source: i0.VersionedTable( | ||||
|         entityName: 'sn_local_sticker_pack', | ||||
|         withoutRowId: false, | ||||
|         isStrict: false, | ||||
|         tableConstraints: [], | ||||
|         columns: [ | ||||
|           _column_0, | ||||
|           _column_2, | ||||
|           _column_3, | ||||
|         ], | ||||
|         attachedDatabase: database, | ||||
|       ), | ||||
|       alias: null); | ||||
|   final i1.Index idxChannelAlias = i1.Index('idx_channel_alias', | ||||
|       'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)'); | ||||
|   final i1.Index idxChatChannel = i1.Index('idx_chat_channel', | ||||
|       'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)'); | ||||
|   final i1.Index idxAccountName = i1.Index('idx_account_name', | ||||
|       'CREATE INDEX idx_account_name ON sn_local_account (name)'); | ||||
|   final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid', | ||||
|       'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)'); | ||||
|   final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account', | ||||
|       'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)'); | ||||
| } | ||||
|  | ||||
| class Shape3 extends i0.VersionedTable { | ||||
|   Shape3({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<int> get channelId => | ||||
|       columnsByName['channel_id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<int> get senderId => | ||||
|       columnsByName['sender_id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<int> _column_10(String aliasedName) => | ||||
|     i1.GeneratedColumn<int>('sender_id', aliasedName, true, | ||||
|         type: i1.DriftSqlType.int); | ||||
|  | ||||
| class Shape4 extends i0.VersionedTable { | ||||
|   Shape4({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<int> get channelId => | ||||
|       columnsByName['channel_id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<int> get accountId => | ||||
|       columnsByName['account_id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
|   i1.GeneratedColumn<DateTime> get cacheExpiredAt => | ||||
|       columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<DateTime> _column_11(String aliasedName) => | ||||
|     i1.GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false, | ||||
|         type: i1.DriftSqlType.dateTime); | ||||
|  | ||||
| class Shape5 extends i0.VersionedTable { | ||||
|   Shape5({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get name => | ||||
|       columnsByName['name']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
|   i1.GeneratedColumn<DateTime> get cacheExpiredAt => | ||||
|       columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<String> _column_12(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('name', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string); | ||||
|  | ||||
| class Shape6 extends i0.VersionedTable { | ||||
|   Shape6({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get rid => | ||||
|       columnsByName['rid']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<String> get uuid => | ||||
|       columnsByName['uuid']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<int> get accountId => | ||||
|       columnsByName['account_id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
|   i1.GeneratedColumn<DateTime> get cacheExpiredAt => | ||||
|       columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<String> _column_13(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('rid', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string, | ||||
|         defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE')); | ||||
| i1.GeneratedColumn<String> _column_14(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('uuid', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string, | ||||
|         defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE')); | ||||
|  | ||||
| class Shape7 extends i0.VersionedTable { | ||||
|   Shape7({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get alias => | ||||
|       columnsByName['alias']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<String> get fullAlias => | ||||
|       columnsByName['full_alias']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i1.GeneratedColumn<String> _column_15(String aliasedName) => | ||||
|     i1.GeneratedColumn<String>('full_alias', aliasedName, false, | ||||
|         type: i1.DriftSqlType.string); | ||||
|  | ||||
| class Shape8 extends i0.VersionedTable { | ||||
|   Shape8({required super.source, required super.alias}) : super.aliased(); | ||||
|   i1.GeneratedColumn<int> get id => | ||||
|       columnsByName['id']! as i1.GeneratedColumn<int>; | ||||
|   i1.GeneratedColumn<String> get content => | ||||
|       columnsByName['content']! as i1.GeneratedColumn<String>; | ||||
|   i1.GeneratedColumn<DateTime> get createdAt => | ||||
|       columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; | ||||
| } | ||||
|  | ||||
| i0.MigrationStepWithVersion migrationSteps({ | ||||
|   required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, | ||||
|   required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, | ||||
| }) { | ||||
|   return (currentVersion, database) async { | ||||
|     switch (currentVersion) { | ||||
|       case 1: | ||||
|         final schema = Schema2(database: database); | ||||
|         final migrator = i1.Migrator(database, schema); | ||||
|         await from1To2(migrator, schema); | ||||
|         return 2; | ||||
|       case 2: | ||||
|         final schema = Schema3(database: database); | ||||
|         final migrator = i1.Migrator(database, schema); | ||||
|         await from2To3(migrator, schema); | ||||
|         return 3; | ||||
|       default: | ||||
|         throw ArgumentError.value('Unknown migration from $currentVersion'); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  | ||||
| i1.OnUpgrade stepByStep({ | ||||
|   required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, | ||||
|   required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, | ||||
| }) => | ||||
|     i0.VersionedSchema.stepByStepHelper( | ||||
|         step: migrationSteps( | ||||
|       from1To2: from1To2, | ||||
|       from2To3: from2To3, | ||||
|     )); | ||||
							
								
								
									
										16
									
								
								lib/database/keypair.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/database/keypair.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
|  | ||||
| class SnLocalKeyPair extends Table { | ||||
|   TextColumn get id => text()(); | ||||
|  | ||||
|   IntColumn get accountId => integer()(); | ||||
|  | ||||
|   TextColumn get publicKey => text()(); | ||||
|  | ||||
|   TextColumn get privateKey => text().nullable()(); | ||||
|  | ||||
|   BoolColumn get isActive => boolean().withDefault(Constant(false))(); | ||||
|  | ||||
|   @override | ||||
|   Set<Column<Object>> get primaryKey => {id}; | ||||
| } | ||||
							
								
								
									
										74
									
								
								lib/database/sticker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								lib/database/sticker.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
|  | ||||
| class SnStickerConverter extends TypeConverter<SnSticker, String> | ||||
|     with JsonTypeConverter2<SnSticker, String, Map<String, Object?>> { | ||||
|   const SnStickerConverter(); | ||||
|  | ||||
|   @override | ||||
|   SnSticker fromSql(String fromDb) { | ||||
|     return fromJson(jsonDecode(fromDb) as Map<String, dynamic>); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toSql(SnSticker value) { | ||||
|     return jsonEncode(toJson(value)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   SnSticker fromJson(Map<String, Object?> json) { | ||||
|     return SnSticker.fromJson(json); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Map<String, Object?> toJson(SnSticker value) { | ||||
|     return value.toJson(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class SnLocalSticker extends Table { | ||||
|   IntColumn get id => integer().autoIncrement()(); | ||||
|  | ||||
|   TextColumn get alias => text()(); | ||||
|  | ||||
|   TextColumn get fullAlias => text()(); | ||||
|  | ||||
|   TextColumn get content => text().map(const SnStickerConverter())(); | ||||
|  | ||||
|   DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); | ||||
| } | ||||
|  | ||||
| class SnStickerPackConverter extends TypeConverter<SnStickerPack, String> | ||||
|     with JsonTypeConverter2<SnStickerPack, String, Map<String, Object?>> { | ||||
|   const SnStickerPackConverter(); | ||||
|  | ||||
|   @override | ||||
|   SnStickerPack fromSql(String fromDb) { | ||||
|     return fromJson(jsonDecode(fromDb) as Map<String, dynamic>); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toSql(SnStickerPack value) { | ||||
|     return jsonEncode(toJson(value)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   SnStickerPack fromJson(Map<String, Object?> json) { | ||||
|     return SnStickerPack.fromJson(json); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Map<String, Object?> toJson(SnStickerPack value) { | ||||
|     return value.toJson(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class SnLocalStickerPack extends Table { | ||||
|   IntColumn get id => integer().autoIncrement()(); | ||||
|  | ||||
|   TextColumn get content => text().map(const SnStickerPackConverter())(); | ||||
|  | ||||
|   DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); | ||||
| } | ||||
							
								
								
									
										10
									
								
								lib/logger.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/logger.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import 'package:talker/talker.dart'; | ||||
|  | ||||
| final logging = Talker( | ||||
|   settings: TalkerSettings( | ||||
|     enabled: true, | ||||
|     useHistory: true, | ||||
|     maxHistoryItems: 1000, | ||||
|     useConsoleLogs: true, | ||||
|   ), | ||||
| ); | ||||
| @@ -20,10 +20,12 @@ import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:surface/firebase_options.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/database.dart'; | ||||
| import 'package:surface/providers/keypair.dart'; | ||||
| import 'package:surface/providers/link_preview.dart'; | ||||
| import 'package:surface/providers/navigation.dart'; | ||||
| import 'package:surface/providers/notification.dart'; | ||||
| @@ -160,6 +162,7 @@ class SolianApp extends StatelessWidget { | ||||
|             Provider(create: (ctx) => SnStickerProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)), | ||||
|             Provider(create: (ctx) => KeyPairProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), | ||||
| @@ -235,7 +238,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { | ||||
|           await inAppReview.requestReview(); | ||||
|           prefs.setBool('rating_requested', true); | ||||
|         } else { | ||||
|           log('Unable request app review, unavailable'); | ||||
|           logging.error('Unable request app review, unavailable'); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
| @@ -263,17 +266,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { | ||||
|           int.tryParse(remoteVersionString.split('+').last) ?? 0; | ||||
|       final localBuildNumber = | ||||
|           int.tryParse(localVersionString.split('+').last) ?? 0; | ||||
|       log("[Update] Local: $localVersionString, Remote: $remoteVersionString"); | ||||
|       logging.info( | ||||
|           "[Update] Local: $localVersionString, Remote: $remoteVersionString"); | ||||
|       if ((remoteVersion > localVersion || | ||||
|               remoteBuildNumber > localBuildNumber) && | ||||
|           mounted) { | ||||
|         final config = context.read<ConfigProvider>(); | ||||
|         config.setUpdate( | ||||
|             remoteVersionString, resp.data?['body'] ?? 'No changelog'); | ||||
|         log("[Update] Update available: $remoteVersionString"); | ||||
|         logging.info("[Update] Update available: $remoteVersionString"); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       log('[Error] Unable to check update: $e'); | ||||
|       logging.error('[Error] Unable to check update...', e); | ||||
|       if (mounted) context.showErrorDialog('Unable to check update: $e'); | ||||
|     } | ||||
|   } | ||||
| @@ -304,9 +308,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { | ||||
|       notify.listen(); | ||||
|       await notify.registerPushNotifications(); | ||||
|       if (!mounted) return; | ||||
|       final kp = context.read<KeyPairProvider>(); | ||||
|       await kp.reloadActive(); | ||||
|       kp.listen(); | ||||
|       if (!mounted) return; | ||||
|       final sticker = context.read<SnStickerProvider>(); | ||||
|       await sticker.listSticker(); | ||||
|       log('[Bootstrap] Everything initialized!'); | ||||
|       if (!mounted) return; | ||||
|       final ud = context.read<UserDirectoryProvider>(); | ||||
|       final userCacheSize = await ud.loadAccountCache(); | ||||
|       logging.info('[Users] Loaded local user cache, size: $userCacheSize'); | ||||
|       logging.info('[Bootstrap] Everything initialized!'); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       await context.showErrorDialog(err); | ||||
| @@ -483,6 +495,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { | ||||
|           WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|             cfg.calcDrawerSize(context); | ||||
|           }); | ||||
|           Future.delayed(const Duration(milliseconds: 300), () { | ||||
|             if (context.mounted) { | ||||
|               cfg.calcDrawerSize(context); | ||||
|             } | ||||
|           }); | ||||
|           return SizeChangedLayoutNotifier( | ||||
|             child: widget.child, | ||||
|           ); | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:surface/providers/database.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/sn_realm.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
|  | ||||
| class ChatChannelProvider extends ChangeNotifier { | ||||
| @@ -15,12 +16,14 @@ class ChatChannelProvider extends ChangeNotifier { | ||||
|  | ||||
|   late final SnNetworkProvider _sn; | ||||
|   late final UserDirectoryProvider _ud; | ||||
|   late final UserProvider _ua; | ||||
|   late final DatabaseProvider _dt; | ||||
|   late final SnRealmProvider _rels; | ||||
|  | ||||
|   ChatChannelProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|     _ud = context.read<UserDirectoryProvider>(); | ||||
|     _ua = context.read<UserProvider>(); | ||||
|     _dt = context.read<DatabaseProvider>(); | ||||
|     _rels = context.read<SnRealmProvider>(); | ||||
|   } | ||||
| @@ -149,4 +152,60 @@ class ChatChannelProvider extends ChangeNotifier { | ||||
|     await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet()); | ||||
|     return out; | ||||
|   } | ||||
|  | ||||
|   Future<void> _saveMemberToLocal(Iterable<SnChannelMember> members) async { | ||||
|     final queries = members.map((ele) { | ||||
|       return _dt.db.snLocalChannelMember.insertOne( | ||||
|         SnLocalChannelMemberCompanion.insert( | ||||
|           id: Value(ele.id), | ||||
|           channelId: ele.channelId, | ||||
|           accountId: ele.accountId, | ||||
|           content: ele, | ||||
|           cacheExpiredAt: DateTime.now().add(const Duration(days: 7)), | ||||
|         ), | ||||
|         onConflict: DoUpdate( | ||||
|           (_) => SnLocalChannelMemberCompanion.custom( | ||||
|             content: Constant(jsonEncode(ele.toJson())), | ||||
|             cacheExpiredAt: | ||||
|                 Constant(DateTime.now().add(const Duration(days: 7))), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     }); | ||||
|     await Future.wait(queries); | ||||
|   } | ||||
|  | ||||
|   Future<void> removeLocalChannel(SnChannel channel) async { | ||||
|     await _dt.db.transaction(() async { | ||||
|       await (_dt.db.snLocalChannelMember.delete() | ||||
|             ..where((e) => e.channelId.equals(channel.id))) | ||||
|           .go(); | ||||
|       await (_dt.db.snLocalChatChannel.delete() | ||||
|             ..where((e) => e.id.equals(channel.id))) | ||||
|           .go(); | ||||
|       await (_dt.db.snLocalChatMessage.delete() | ||||
|             ..where((e) => e.channelId.equals(channel.id))) | ||||
|           .go(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> updateChannelProfile(SnChannelMember member) { | ||||
|     return _saveMemberToLocal([member]); | ||||
|   } | ||||
|  | ||||
|   Future<SnChannelMember> getChannelProfile(SnChannel channel) async { | ||||
|     if (_ua.user == null) throw Exception('User not logged in'); | ||||
|     final local = await (_dt.db.snLocalChannelMember.select() | ||||
|           ..where((e) => e.channelId.equals(channel.id)) | ||||
|           ..where((e) => e.accountId.equals(_ua.user!.id))) | ||||
|         .getSingleOrNull(); | ||||
|     if (local != null) { | ||||
|       return local.content; | ||||
|     } | ||||
|  | ||||
|     final resp = await _sn.client.get('/cgi/im/channels/${channel.keyPath}/me'); | ||||
|     final out = SnChannelMember.fromJson(resp.data); | ||||
|     _saveMemberToLocal([out]); | ||||
|     return out; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -18,6 +18,7 @@ const kAppNotifyWithHaptic = 'app_notify_with_haptic'; | ||||
| const kAppExpandPostLink = 'app_expand_post_link'; | ||||
| const kAppExpandChatLink = 'app_expand_chat_link'; | ||||
| const kAppRealmCompactView = 'app_realm_compact_view'; | ||||
| const kAppCustomFonts = 'app_custom_fonts'; | ||||
|  | ||||
| const Map<String, FilterQuality> kImageQualityLevel = { | ||||
|   'settingsImageQualityLowest': FilterQuality.none, | ||||
|   | ||||
							
								
								
									
										245
									
								
								lib/providers/keypair.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								lib/providers/keypair.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/database/database.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/database.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
| import 'package:surface/types/keypair.dart'; | ||||
| import 'package:fast_rsa/fast_rsa.dart'; | ||||
| import 'package:surface/types/websocket.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| // Currently the keypair only provide RSA encryption | ||||
| // Supported by the `fast_rsa` package | ||||
| class KeyPairProvider { | ||||
|   late final DatabaseProvider _dt; | ||||
|   late final UserProvider _ua; | ||||
|   late final WebSocketProvider _ws; | ||||
|  | ||||
|   SnKeyPair? activeKp; | ||||
|  | ||||
|   KeyPairProvider(BuildContext context) { | ||||
|     _dt = context.read<DatabaseProvider>(); | ||||
|     _ua = context.read<UserProvider>(); | ||||
|     _ws = context.read<WebSocketProvider>(); | ||||
|   } | ||||
|  | ||||
|   void listen() { | ||||
|     _ws.pk.stream.listen((event) { | ||||
|       switch (event.method) { | ||||
|         case 'kex.ack': | ||||
|           ackKeyExchange(event); | ||||
|           break; | ||||
|         case 'kex.ask': | ||||
|           replyAskKeyExchange(event); | ||||
|           break; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<String> decryptText(String text, String kpId, {int? kpOwner}) async { | ||||
|     String? publicKey; | ||||
|     final kp = await (_dt.db.snLocalKeyPair.select() | ||||
|           ..where((e) => e.id.equals(kpId))) | ||||
|         .getSingleOrNull(); | ||||
|     if (kp == null) { | ||||
|       if (kpOwner != null) { | ||||
|         final out = await askKeyExchange(kpOwner, kpId); | ||||
|         publicKey = out.publicKey; | ||||
|       } | ||||
|     } else { | ||||
|       publicKey = kp.publicKey; | ||||
|     } | ||||
|     if (publicKey == null) { | ||||
|       throw Exception('Key pair not found'); | ||||
|     } | ||||
|     return await RSA.decryptPKCS1v15(text, publicKey); | ||||
|   } | ||||
|  | ||||
|   Future<String> encryptText(String text) async { | ||||
|     if (activeKp == null) throw Exception('No active key pair'); | ||||
|     return await RSA.encryptPKCS1v15(text, activeKp!.privateKey!); | ||||
|   } | ||||
|  | ||||
|   final Map<String, Completer<SnKeyPair>> _requests = {}; | ||||
|  | ||||
|   Future<SnKeyPair> askKeyExchange(int kpOwner, String kpId) async { | ||||
|     if (_requests.containsKey(kpId)) return await _requests[kpId]!.future; | ||||
|  | ||||
|     final completer = Completer<SnKeyPair>(); | ||||
|     _requests[kpId] = completer; | ||||
|  | ||||
|     _ws.conn?.sink.add( | ||||
|       jsonEncode(WebSocketPackage( | ||||
|         method: 'kex.ask', | ||||
|         endpoint: 'id', | ||||
|         payload: { | ||||
|           'keypair_id': kpId, | ||||
|           'user_id': kpOwner, | ||||
|         }, | ||||
|       )), | ||||
|     ); | ||||
|  | ||||
|     return Future.any([ | ||||
|       _requests[kpId]!.future, | ||||
|       Future.delayed(const Duration(seconds: 60), () { | ||||
|         _requests.remove(kpId); | ||||
|         throw TimeoutException("Key exchange timed out"); | ||||
|       }), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   Future<void> ackKeyExchange(WebSocketPackage pkt) async { | ||||
|     if (pkt.payload == null) return; | ||||
|     final kpMeta = SnKeyPair( | ||||
|       id: pkt.payload!['keypair_id'] as String, | ||||
|       accountId: pkt.payload!['user_id'] as int, | ||||
|       publicKey: pkt.payload!['public_key'] as String, | ||||
|       privateKey: pkt.payload?['private_key'] as String?, | ||||
|     ); | ||||
|  | ||||
|     if (_requests.containsKey(kpMeta.id)) { | ||||
|       _requests[kpMeta.id]!.complete(kpMeta); | ||||
|       _requests.remove(kpMeta.id); | ||||
|     } | ||||
|  | ||||
|     // Save the keypair to the local database | ||||
|     await _dt.db.snLocalKeyPair.insertOne( | ||||
|       SnLocalKeyPairCompanion.insert( | ||||
|         id: kpMeta.id, | ||||
|         accountId: kpMeta.accountId, | ||||
|         publicKey: kpMeta.publicKey, | ||||
|         privateKey: Value(kpMeta.privateKey), | ||||
|       ), | ||||
|       onConflict: DoNothing(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> replyAskKeyExchange(WebSocketPackage pkt) async { | ||||
|     final kpId = pkt.payload!['keypair_id'] as String; | ||||
|     final userId = pkt.payload!['user_id'] as int; | ||||
|     final clientId = pkt.payload!['client_id'] as String; | ||||
|  | ||||
|     final localKp = await (_dt.db.snLocalKeyPair.select() | ||||
|           ..where((e) => e.id.equals(kpId)) | ||||
|           ..limit(1)) | ||||
|         .getSingleOrNull(); | ||||
|     if (localKp == null) return; | ||||
|  | ||||
|     logging.info( | ||||
|       '[Kex] Reply to key exchange request of $kpId from user $userId', | ||||
|     ); | ||||
|  | ||||
|     // We do not give the private key to the client | ||||
|     _ws.conn?.sink.add(jsonEncode( | ||||
|       WebSocketPackage( | ||||
|         method: 'kex.ack', | ||||
|         endpoint: 'id', | ||||
|         payload: { | ||||
|           'keypair_id': localKp.id, | ||||
|           'user_id': localKp.accountId, | ||||
|           'public_key': localKp.publicKey, | ||||
|           'client_id': clientId, | ||||
|         }, | ||||
|       ).toJson(), | ||||
|     )); | ||||
|   } | ||||
|  | ||||
|   Future<SnKeyPair?> reloadActive({bool autoEnroll = true}) async { | ||||
|     final kp = await (_dt.db.snLocalKeyPair.select() | ||||
|           ..where((e) => e.accountId.equals(_ua.user!.id)) | ||||
|           ..where((e) => e.privateKey.isNotNull()) | ||||
|           ..where((e) => e.isActive.equals(true)) | ||||
|           ..limit(1)) | ||||
|         .getSingleOrNull(); | ||||
|  | ||||
|     if (kp != null) { | ||||
|       activeKp = SnKeyPair( | ||||
|         id: kp.id, | ||||
|         accountId: kp.accountId, | ||||
|         publicKey: kp.publicKey, | ||||
|         privateKey: kp.privateKey, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (kp == null && autoEnroll) { | ||||
|       return await enrollNew(); | ||||
|     } | ||||
|  | ||||
|     return activeKp; | ||||
|   } | ||||
|  | ||||
|   Future<List<SnKeyPair>> listKeyPair() async { | ||||
|     final kps = await (_dt.db.snLocalKeyPair.select()).get(); | ||||
|     return kps | ||||
|         .map((e) => SnKeyPair( | ||||
|               id: e.id, | ||||
|               accountId: e.accountId, | ||||
|               publicKey: e.publicKey, | ||||
|               privateKey: e.privateKey, | ||||
|               isActive: e.isActive, | ||||
|             )) | ||||
|         .toList(); | ||||
|   } | ||||
|  | ||||
|   Future<void> activeKeyPair(String kpId) async { | ||||
|     final kp = await (_dt.db.snLocalKeyPair.select() | ||||
|           ..where((e) => e.id.equals(kpId)) | ||||
|           ..where((e) => e.privateKey.isNotNull()) | ||||
|           ..limit(1)) | ||||
|         .getSingleOrNull(); | ||||
|     if (kp == null) return; | ||||
|  | ||||
|     await _dt.db.transaction(() async { | ||||
|       await (_dt.db.update(_dt.db.snLocalKeyPair) | ||||
|             ..where((e) => e.isActive.equals(true))) | ||||
|           .write(SnLocalKeyPairCompanion(isActive: Value(false))); | ||||
|  | ||||
|       await (_dt.db.update(_dt.db.snLocalKeyPair) | ||||
|             ..where((e) => e.id.equals(kp.id))) | ||||
|           .write(SnLocalKeyPairCompanion(isActive: Value(true))); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<SnKeyPair> enrollNew() async { | ||||
|     if (!_ua.isAuthorized) throw Exception('Unauthorized'); | ||||
|  | ||||
|     final id = const Uuid().v4(); | ||||
|     final kp = await RSA.generate(2048); | ||||
|     final kpMeta = SnKeyPair( | ||||
|       id: id, | ||||
|       accountId: _ua.user!.id, | ||||
|       // This is work as expected | ||||
|       // We need to share private key to let everyone can decode the message | ||||
|       publicKey: kp.privateKey, | ||||
|       privateKey: kp.publicKey, | ||||
|     ); | ||||
|  | ||||
|     // Save the keypair to the local database | ||||
|     // If there is already one with private key, it will be overwritten | ||||
|     await _dt.db.transaction(() async { | ||||
|       await (_dt.db.update(_dt.db.snLocalKeyPair) | ||||
|             ..where((e) => e.isActive.equals(true))) | ||||
|           .write(SnLocalKeyPairCompanion(isActive: Value(false))); | ||||
|  | ||||
|       await _dt.db.snLocalKeyPair.insertOne( | ||||
|         SnLocalKeyPairCompanion.insert( | ||||
|           id: kpMeta.id, | ||||
|           accountId: kpMeta.accountId, | ||||
|           publicKey: kpMeta.publicKey, | ||||
|           privateKey: Value(kpMeta.privateKey), | ||||
|           isActive: Value(true), | ||||
|         ), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     await reloadActive(autoEnroll: false); | ||||
|  | ||||
|     return kpMeta; | ||||
|   } | ||||
| } | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/link.dart'; | ||||
|  | ||||
| @@ -20,7 +20,7 @@ class SnLinkPreviewProvider { | ||||
|     final target = b64.encode(url); | ||||
|     if (_cache.containsKey(target)) return _cache[target]; | ||||
|  | ||||
|     log('[LinkPreview] Fetching $url ($target)'); | ||||
|     logging.debug('[LinkPreview] Fetching $url ($target)'); | ||||
|  | ||||
|     try { | ||||
|       final resp = await _sn.client.get('/cgi/re/link/$target'); | ||||
| @@ -28,7 +28,7 @@ class SnLinkPreviewProvider { | ||||
|       _cache[url] = meta; | ||||
|       return meta; | ||||
|     } catch (err) { | ||||
|       log('[LinkPreview] Failed to fetch $url ($target)...'); | ||||
|       logging.warning('[LinkPreview] Failed to fetch $url ($target)...', err); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| @@ -9,6 +8,7 @@ import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_udid/flutter_udid.dart'; | ||||
| import 'package:local_notifier/local_notifier.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| @@ -48,11 +48,13 @@ class NotificationProvider extends ChangeNotifier { | ||||
|     var deviceUuid = await FlutterUdid.consistentUdid; | ||||
|  | ||||
|     if (deviceUuid.isEmpty) { | ||||
|       log("Unable to active push notifications, couldn't get device uuid"); | ||||
|       logging.warning( | ||||
|           '[Push Notification] Unable to active push notifications, couldn\'t get device uuid'); | ||||
|       return; | ||||
|     } else { | ||||
|       log('Device UUID is $deviceUuid'); | ||||
|       log('Registering device push notifications...'); | ||||
|       logging.info('[Push Notification] Device UUID is $deviceUuid'); | ||||
|       logging | ||||
|           .info('[Push Notification] Registering device push notifications...'); | ||||
|     } | ||||
|  | ||||
|     if (Platform.isIOS || Platform.isMacOS) { | ||||
| @@ -62,7 +64,7 @@ class NotificationProvider extends ChangeNotifier { | ||||
|       provider = 'fcm'; | ||||
|       token = await FirebaseMessaging.instance.getToken(); | ||||
|     } | ||||
|     log('Device Push Token is $token'); | ||||
|     logging.info('[Push Notification] Device Push Token is $token'); | ||||
|  | ||||
|     await _sn.client.post( | ||||
|       '/cgi/id/notifications/subscription', | ||||
|   | ||||
| @@ -28,6 +28,7 @@ class SnPostContentProvider { | ||||
|  | ||||
|   Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async { | ||||
|     Set<String> rids = {}; | ||||
|     Set<int> uids = {}; | ||||
|     for (var i = 0; i < out.length; i++) { | ||||
|       rids.addAll(out[i].body['attachments']?.cast<String>() ?? []); | ||||
|       if (out[i].body['thumbnail'] != null) { | ||||
| @@ -41,6 +42,9 @@ class SnPostContentProvider { | ||||
|           repostTo: await _preloadRelatedDataSingle(out[i].repostTo!), | ||||
|         ); | ||||
|       } | ||||
|       if (out[i].publisher.type == 0) { | ||||
|         uids.add(out[i].publisher.accountId); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     final attachments = await _attach.getMultiple(rids.toList()); | ||||
| @@ -65,15 +69,15 @@ class SnPostContentProvider { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     await _ud.listAccount( | ||||
|       attachments.where((ele) => ele != null).map((ele) => ele!.accountId).toSet(), | ||||
|     ); | ||||
|     uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId)); | ||||
|     await _ud.listAccount(uids); | ||||
|  | ||||
|     return out; | ||||
|   } | ||||
|  | ||||
|   Future<SnPost> _preloadRelatedDataSingle(SnPost out) async { | ||||
|     Set<String> rids = {}; | ||||
|     Set<int> uids = {}; | ||||
|     rids.addAll(out.body['attachments']?.cast<String>() ?? []); | ||||
|     if (out.body['thumbnail'] != null) { | ||||
|       rids.add(out.body['thumbnail']); | ||||
| @@ -86,6 +90,9 @@ class SnPostContentProvider { | ||||
|         repostTo: await _preloadRelatedDataSingle(out.repostTo!), | ||||
|       ); | ||||
|     } | ||||
|     if (out.publisher.type == 0) { | ||||
|       uids.add(out.publisher.accountId); | ||||
|     } | ||||
|  | ||||
|     final attachments = await _attach.getMultiple(rids.toList()); | ||||
|  | ||||
| @@ -108,6 +115,9 @@ class SnPostContentProvider { | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId)); | ||||
|     await _ud.listAccount(uids); | ||||
|  | ||||
|     return out; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| import 'dart:collection'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:math' as math; | ||||
| import 'dart:typed_data'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:cross_file/cross_file.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/database/database.dart'; | ||||
| import 'package:surface/providers/database.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
|  | ||||
| @@ -13,10 +16,12 @@ const kConcurrentUploadChunks = 5; | ||||
|  | ||||
| class SnAttachmentProvider { | ||||
|   late final SnNetworkProvider _sn; | ||||
|   late final DatabaseProvider _dt; | ||||
|   final Map<String, SnAttachment> _cache = {}; | ||||
|  | ||||
|   SnAttachmentProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|     _dt = context.read<DatabaseProvider>(); | ||||
|   } | ||||
|  | ||||
|   void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) { | ||||
| @@ -28,21 +33,33 @@ class SnAttachmentProvider { | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment> getOne(String rid, {noCache = false}) async { | ||||
|     // In-memory cache | ||||
|     if (!noCache && _cache.containsKey(rid)) { | ||||
|       return _cache[rid]!; | ||||
|     } | ||||
|  | ||||
|     // On-disk cache | ||||
|     final dbResp = await (_dt.db.snLocalAttachment.select() | ||||
|           ..where((e) => e.rid.equals(rid)) | ||||
|           ..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now()))) | ||||
|         .getSingleOrNull(); | ||||
|     if (dbResp != null) { | ||||
|       _cache[rid] = dbResp.content; | ||||
|       return dbResp.content; | ||||
|     } | ||||
|     // Remote server | ||||
|     final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta'); | ||||
|     final out = SnAttachment.fromJson(resp.data); | ||||
|     if (out.isAnalyzed) { | ||||
|       _cache[rid] = out; | ||||
|     } | ||||
|     _saveToLocal([out]); | ||||
|  | ||||
|     return out; | ||||
|   } | ||||
|  | ||||
|   Future<List<SnAttachment?>> getMultiple(List<String> rids, | ||||
|       {noCache = false}) async { | ||||
|       {bool noCache = false}) async { | ||||
|     // In-memory cache | ||||
|     final result = List<SnAttachment?>.filled(rids.length, null); | ||||
|     final Map<String, int> randomMapping = {}; | ||||
|     for (int i = 0; i < rids.length; i++) { | ||||
| @@ -53,29 +70,44 @@ class SnAttachmentProvider { | ||||
|         result[i] = _cache[rid]!; | ||||
|       } | ||||
|     } | ||||
|     final pendingFetch = randomMapping.keys; | ||||
|  | ||||
|     if (pendingFetch.isNotEmpty) { | ||||
|       final resp = await _sn.client.get( | ||||
|         '/cgi/uc/attachments', | ||||
|         queryParameters: { | ||||
|           'take': pendingFetch.length, | ||||
|           'id': pendingFetch.join(','), | ||||
|         }, | ||||
|       ); | ||||
|       final List<SnAttachment?> out = resp.data['data'] | ||||
|           .map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)) | ||||
|           .cast<SnAttachment?>() | ||||
|           .toList(); | ||||
|  | ||||
|       for (final item in out) { | ||||
|         if (item == null) continue; | ||||
|         if (item.isAnalyzed) { | ||||
|           _cache[item.rid] = item; | ||||
|     var pendingFetch = randomMapping.keys; | ||||
|     // On-disk cache | ||||
|     if (pendingFetch.isEmpty) return result; | ||||
|     if (!noCache) { | ||||
|       final dbResp = await (_dt.db.snLocalAttachment.select() | ||||
|             ..where((e) => e.rid.isIn(pendingFetch)) | ||||
|             ..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now()))) | ||||
|           .get(); | ||||
|       for (final item in dbResp) { | ||||
|         if (item.content.isAnalyzed) { | ||||
|           _cache[item.rid] = item.content; | ||||
|         } | ||||
|         result[randomMapping[item.rid]!] = item; | ||||
|         result[randomMapping[item.rid]!] = item.content; | ||||
|         randomMapping.remove(item.rid); | ||||
|       } | ||||
|       pendingFetch = randomMapping.keys; | ||||
|     } | ||||
|     // Remote server | ||||
|     if (pendingFetch.isEmpty) return result; | ||||
|     final resp = await _sn.client.get( | ||||
|       '/cgi/uc/attachments', | ||||
|       queryParameters: { | ||||
|         'take': pendingFetch.length, | ||||
|         'id': pendingFetch.join(','), | ||||
|       }, | ||||
|     ); | ||||
|     final List<SnAttachment?> out = resp.data['data'] | ||||
|         .map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)) | ||||
|         .cast<SnAttachment?>() | ||||
|         .toList(); | ||||
|     for (final item in out) { | ||||
|       if (item == null) continue; | ||||
|       if (item.isAnalyzed) { | ||||
|         _cache[item.rid] = item; | ||||
|       } | ||||
|       result[randomMapping[item.rid]!] = item; | ||||
|     } | ||||
|     _saveToLocal(out.where((ele) => ele != null).cast()); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
| @@ -274,6 +306,31 @@ class SnAttachmentProvider { | ||||
|       'metadata': metadata ?? item.usermeta, | ||||
|       'is_indexable': isIndexable ?? item.isIndexable, | ||||
|     }); | ||||
|     return SnAttachment.fromJson(resp.data); | ||||
|     final out = SnAttachment.fromJson(resp.data); | ||||
|     _saveToLocal([out]); | ||||
|     return out; | ||||
|   } | ||||
|  | ||||
|   Future<void> _saveToLocal(Iterable<SnAttachment> out) async { | ||||
|     for (final ele in out) { | ||||
|       if (!ele.isAnalyzed || ele.destination == 0) continue; | ||||
|       await _dt.db.snLocalAttachment.insertOne( | ||||
|         SnLocalAttachmentCompanion.insert( | ||||
|           id: Value(ele.id), | ||||
|           rid: ele.rid, | ||||
|           uuid: ele.uuid, | ||||
|           content: ele, | ||||
|           accountId: ele.accountId, | ||||
|           cacheExpiredAt: DateTime.now().add(const Duration(days: 7)), | ||||
|         ), | ||||
|         onConflict: DoUpdate( | ||||
|           (_) => SnLocalAttachmentCompanion.custom( | ||||
|             content: Constant(jsonEncode(ele.toJson())), | ||||
|             cacheExpiredAt: | ||||
|                 Constant(DateTime.now().add(const Duration(days: 7))), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| @@ -11,9 +10,12 @@ import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/widget.dart'; | ||||
| import 'package:synchronized/synchronized.dart'; | ||||
| import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart'; | ||||
| import 'package:talker_dio_logger/talker_dio_logger_settings.dart'; | ||||
|  | ||||
| const kNetworkServerDirectory = [ | ||||
|   ('Solar Network', 'https://api.sn.solsynth.dev'), | ||||
| @@ -36,6 +38,19 @@ class SnNetworkProvider { | ||||
|  | ||||
|     client = Dio(); | ||||
|  | ||||
|     client.interceptors.add( | ||||
|       TalkerDioLogger( | ||||
|         talker: logging, | ||||
|         settings: const TalkerDioLoggerSettings( | ||||
|           printRequestHeaders: false, | ||||
|           printResponseHeaders: false, | ||||
|           printResponseMessage: false, | ||||
|           printResponseData: false, | ||||
|           printRequestData: false, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     client.interceptors.add(RetryInterceptor( | ||||
|       dio: client, | ||||
|       retries: 3, | ||||
| @@ -69,7 +84,6 @@ class SnNetworkProvider { | ||||
|       _prefs = _config.prefs; | ||||
|       client.options.baseUrl = _config.serverUrl; | ||||
|     }); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   static Future<Dio> createOffContextClient() async { | ||||
| @@ -91,7 +105,8 @@ class SnNetworkProvider { | ||||
|           RequestOptions options, | ||||
|           RequestInterceptorHandler handler, | ||||
|         ) async { | ||||
|           final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), prefs.getString(kRtkStoreKey), (atk, rtk) { | ||||
|           final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), | ||||
|               prefs.getString(kRtkStoreKey), (atk, rtk) { | ||||
|             prefs.setString(kAtkStoreKey, atk); | ||||
|             prefs.setString(kRtkStoreKey, rtk); | ||||
|           }); | ||||
| @@ -103,7 +118,8 @@ class SnNetworkProvider { | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|     client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; | ||||
|     client.options.baseUrl = | ||||
|         prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; | ||||
|  | ||||
|     return client; | ||||
|   } | ||||
| @@ -119,7 +135,8 @@ class SnNetworkProvider { | ||||
|       platformInfo = 'Web; ${deviceInfo.vendor}'; | ||||
|     } else if (Platform.isAndroid) { | ||||
|       final deviceInfo = await DeviceInfoPlugin().androidInfo; | ||||
|       platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}'; | ||||
|       platformInfo = | ||||
|           'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}'; | ||||
|     } else if (Platform.isIOS) { | ||||
|       final deviceInfo = await DeviceInfoPlugin().iosInfo; | ||||
|       platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}'; | ||||
| @@ -128,7 +145,8 @@ class SnNetworkProvider { | ||||
|       platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}'; | ||||
|     } else if (Platform.isWindows) { | ||||
|       final deviceInfo = await DeviceInfoPlugin().windowsInfo; | ||||
|       platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}'; | ||||
|       platformInfo = | ||||
|           'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}'; | ||||
|     } else if (Platform.isLinux) { | ||||
|       final deviceInfo = await DeviceInfoPlugin().linuxInfo; | ||||
|       platformInfo = 'Linux; ${deviceInfo.prettyName}'; | ||||
| @@ -148,12 +166,15 @@ class SnNetworkProvider { | ||||
|   final tkLock = Lock(); | ||||
|  | ||||
|   Future<String?> getFreshAtk() async { | ||||
|     return await _getFreshAtk(client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), (atk, rtk) { | ||||
|     return await _getFreshAtk( | ||||
|         client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), | ||||
|         (atk, rtk) { | ||||
|       setTokenPair(atk, rtk); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async { | ||||
|   static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, | ||||
|       Function(String atk, String rtk)? onRefresh) async { | ||||
|     if (_refreshCompleter != null) { | ||||
|       return await _refreshCompleter!.future; | ||||
|     } else { | ||||
| @@ -185,7 +206,8 @@ class SnNetworkProvider { | ||||
|         final payload = b64.decode(rawPayload); | ||||
|         final exp = jsonDecode(payload)['exp']; | ||||
|         if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { | ||||
|           log('Access token need refresh, doing it at ${DateTime.now()}'); | ||||
|           logging.debug( | ||||
|               '[Auth] Access token need refresh, doing it at ${DateTime.now()}'); | ||||
|           final result = await _refreshToken(client.options.baseUrl, rtk); | ||||
|           if (result == null) { | ||||
|             atk = null; | ||||
| @@ -199,12 +221,12 @@ class SnNetworkProvider { | ||||
|           _refreshCompleter!.complete(atk); | ||||
|           return atk; | ||||
|         } else { | ||||
|           log('Access token refresh failed...'); | ||||
|           logging.error('[Auth] Access token refresh failed...'); | ||||
|           _refreshCompleter!.complete(null); | ||||
|         } | ||||
|       } | ||||
|     } catch (err) { | ||||
|       log('Failed to authenticate user: $err'); | ||||
|       logging.error('[Auth] Failed to authenticate user...', err); | ||||
|       _refreshCompleter!.completeError(err); | ||||
|     } finally { | ||||
|       _refreshCompleter = null; | ||||
| @@ -237,7 +259,8 @@ class SnNetworkProvider { | ||||
|     return result.$1; | ||||
|   } | ||||
|  | ||||
|   static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async { | ||||
|   static Future<(String, String)?> _refreshToken( | ||||
|       String baseUrl, String? rtk) async { | ||||
|     if (rtk == null) return null; | ||||
|  | ||||
|     final dio = Dio(); | ||||
|   | ||||
| @@ -1,12 +1,17 @@ | ||||
| import 'dart:developer'; | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/database/database.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/database.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
|  | ||||
| class SnStickerProvider { | ||||
|   late final SnNetworkProvider _sn; | ||||
|   late final DatabaseProvider _dt; | ||||
|   final Map<String, SnSticker?> _cache = {}; | ||||
|  | ||||
|   final Map<int, List<SnSticker>> stickersByPack = {}; | ||||
| @@ -16,6 +21,7 @@ class SnStickerProvider { | ||||
|  | ||||
|   SnStickerProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|     _dt = context.read<DatabaseProvider>(); | ||||
|   } | ||||
|  | ||||
|   bool hasNotSticker(String alias) { | ||||
| @@ -32,32 +38,54 @@ class SnStickerProvider { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void putSticker(Iterable<SnSticker> sticker) { | ||||
|     for (final ele in sticker) { | ||||
|   void putSticker(Iterable<SnSticker> stickers) { | ||||
|     for (final ele in stickers) { | ||||
|       _cacheSticker(ele); | ||||
|     } | ||||
|     _saveStickerToLocal(stickers); | ||||
|     _saveStickerPackToLocal(stickers.map((ele) => ele.pack).toSet()); | ||||
|   } | ||||
|  | ||||
|   Future<SnSticker?> lookupSticker(String alias) async { | ||||
|     // In-memory cache | ||||
|     if (_cache.containsKey(alias)) { | ||||
|       return _cache[alias]; | ||||
|     } | ||||
|  | ||||
|     // On-disk cache | ||||
|     final localStickers = await (_dt.db.snLocalSticker.select() | ||||
|           ..where((e) => e.fullAlias.equals(alias))) | ||||
|         .getSingleOrNull(); | ||||
|     if (localStickers != null) { | ||||
|       _cache[alias] = localStickers.content; | ||||
|       return localStickers.content; | ||||
|     } | ||||
|     // Remote server | ||||
|     try { | ||||
|       final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias'); | ||||
|       final sticker = SnSticker.fromJson(resp.data); | ||||
|       _cacheSticker(sticker); | ||||
|  | ||||
|       putSticker([sticker]); | ||||
|       return sticker; | ||||
|     } catch (err) { | ||||
|       _cache[alias] = null; | ||||
|       log('[Sticker] Failed to lookup sticker $alias: $err'); | ||||
|       logging.warning('[Sticker] Failed to lookup sticker $alias', err); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<void> listSticker() async { | ||||
|     final localPacks = await _dt.db.snLocalStickerPack.select().get(); | ||||
|     final localStickers = await _dt.db.snLocalSticker.select().get(); | ||||
|     final local = localStickers.map((ele) { | ||||
|       return ele.content.copyWith( | ||||
|         pack: localPacks | ||||
|             .firstWhere((pk) => pk.content.id == ele.content.packId) | ||||
|             .content, | ||||
|       ); | ||||
|     }); | ||||
|     for (final sticker in local) { | ||||
|       _cacheSticker(sticker); | ||||
|     } | ||||
|     try { | ||||
|       final resp = await _sn.client.get('/cgi/uc/stickers'); | ||||
|       final data = resp.data; | ||||
| @@ -66,8 +94,39 @@ class SnStickerProvider { | ||||
|         _cacheSticker(sticker); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       log('[Sticker] Failed to list stickers: $err'); | ||||
|       logging.error('[Sticker] Failed to list stickers...', err); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _saveStickerToLocal(Iterable<SnSticker> stickers) async { | ||||
|     await _dt.db.snLocalSticker.insertAll( | ||||
|       stickers.map( | ||||
|         (ele) => SnLocalStickerCompanion.insert( | ||||
|           id: Value(ele.id), | ||||
|           alias: ele.alias, | ||||
|           fullAlias: '${ele.pack.prefix}${ele.alias}', | ||||
|           content: ele, | ||||
|           createdAt: Value(ele.createdAt), | ||||
|         ), | ||||
|       ), | ||||
|       onConflict: DoNothing(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> _saveStickerPackToLocal(Iterable<SnStickerPack> packs) async { | ||||
|     final queries = packs | ||||
|         .map( | ||||
|           (ele) => _dt.db.snLocalStickerPack.insertOne( | ||||
|               SnLocalStickerPackCompanion.insert( | ||||
|                 id: Value(ele.id), | ||||
|                 content: ele, | ||||
|                 createdAt: Value(ele.createdAt), | ||||
|               ), | ||||
|               onConflict: DoUpdate((_) => SnLocalStickerPackCompanion.custom( | ||||
|                   content: Constant(jsonEncode(ele.toJson()))))), | ||||
|         ) | ||||
|         .toList(); | ||||
|     await Future.wait(queries); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,8 +13,16 @@ class ThemeProvider extends ChangeNotifier { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) { | ||||
|     createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) { | ||||
|   void reloadTheme({ | ||||
|     Color? seedColorOverride, | ||||
|     bool? useMaterial3, | ||||
|     String? customFonts, | ||||
|   }) { | ||||
|     createAppThemeSet( | ||||
|       seedColorOverride: seedColorOverride, | ||||
|       useMaterial3: useMaterial3, | ||||
|       customFonts: customFonts, | ||||
|     ).then((value) { | ||||
|       theme = value; | ||||
|       notifyListeners(); | ||||
|     }); | ||||
|   | ||||
| @@ -1,19 +1,36 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/database/database.dart'; | ||||
| import 'package:surface/providers/database.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
|  | ||||
| class UserDirectoryProvider { | ||||
|   late final SnNetworkProvider _sn; | ||||
|   late final DatabaseProvider _dt; | ||||
|  | ||||
|   UserDirectoryProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|     _dt = context.read<DatabaseProvider>(); | ||||
|   } | ||||
|  | ||||
|   final Map<String, int> _idCache = {}; | ||||
|   final Map<int, SnAccount> _cache = {}; | ||||
|  | ||||
|   Future<int> loadAccountCache({int max = 100}) async { | ||||
|     final out = await (_dt.db.snLocalAccount.select()..limit(max)).get(); | ||||
|     for (final ele in out) { | ||||
|       _cache[ele.id] = ele.content; | ||||
|       _idCache[ele.name] = ele.id; | ||||
|     } | ||||
|     return out.length; | ||||
|   } | ||||
|  | ||||
|   Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async { | ||||
|     // In-memory cache | ||||
|     final out = List<SnAccount?>.generate(id.length, (e) => null); | ||||
|     final plannedQuery = <int>{}; | ||||
|     for (var idx = 0; idx < out.length; idx++) { | ||||
| @@ -27,8 +44,29 @@ class UserDirectoryProvider { | ||||
|         plannedQuery.add(item); | ||||
|       } | ||||
|     } | ||||
|     final resp = await _sn.client.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')}); | ||||
|     final respDecoded = resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList(); | ||||
|     // On-disk cache | ||||
|     if (plannedQuery.isEmpty) return out; | ||||
|     final dbResp = await (_dt.db.snLocalAccount.select() | ||||
|           ..where((e) => e.id.isIn(plannedQuery)) | ||||
|           ..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())) | ||||
|           ..limit(plannedQuery.length)) | ||||
|         .get(); | ||||
|     for (var idx = 0; idx < out.length; idx++) { | ||||
|       if (out[idx] != null) continue; | ||||
|       if (dbResp.length <= idx) { | ||||
|         break; | ||||
|       } | ||||
|       out[idx] = dbResp[idx].content; | ||||
|       _cache[dbResp[idx].id] = dbResp[idx].content; | ||||
|       _idCache[dbResp[idx].name] = dbResp[idx].id; | ||||
|       plannedQuery.remove(dbResp[idx].id); | ||||
|     } | ||||
|     // Remote server | ||||
|     if (plannedQuery.isEmpty) return out; | ||||
|     final resp = await _sn.client | ||||
|         .get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')}); | ||||
|     final respDecoded = | ||||
|         resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList(); | ||||
|     var sideIdx = 0; | ||||
|     for (var idx = 0; idx < out.length; idx++) { | ||||
|       if (out[idx] != null) continue; | ||||
| @@ -40,17 +78,29 @@ class UserDirectoryProvider { | ||||
|       _idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id; | ||||
|       sideIdx++; | ||||
|     } | ||||
|     if (respDecoded.isNotEmpty) _saveToLocal(respDecoded); | ||||
|     return out; | ||||
|   } | ||||
|  | ||||
|   Future<SnAccount?> getAccount(dynamic id) async { | ||||
|     // In-memory cache | ||||
|     if (id is String && _idCache.containsKey(id)) { | ||||
|       id = _idCache[id]; | ||||
|     } | ||||
|     if (_cache.containsKey(id)) { | ||||
|       return _cache[id]; | ||||
|     } | ||||
|  | ||||
|     // On-disk cache | ||||
|     final dbResp = await (_dt.db.snLocalAccount.select() | ||||
|           ..where((e) => e.id.equals(id)) | ||||
|           ..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now()))) | ||||
|         .getSingleOrNull(); | ||||
|     if (dbResp != null) { | ||||
|       _cache[dbResp.id] = dbResp.content; | ||||
|       _idCache[dbResp.name] = dbResp.id; | ||||
|       return dbResp.content; | ||||
|     } | ||||
|     // Remote server | ||||
|     try { | ||||
|       final resp = await _sn.client.get('/cgi/id/users/$id'); | ||||
|       final account = SnAccount.fromJson( | ||||
| @@ -58,16 +108,42 @@ class UserDirectoryProvider { | ||||
|       ); | ||||
|       _cache[account.id] = account; | ||||
|       if (id is String) _idCache[id] = account.id; | ||||
|       _saveToLocal([account]); | ||||
|       return account; | ||||
|     } catch (err) { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   SnAccount? getAccountFromCache(dynamic id) { | ||||
|   SnAccount? getFromCache(dynamic id) { | ||||
|     if (id is String && _idCache.containsKey(id)) { | ||||
|       id = _idCache[id]; | ||||
|     } | ||||
|     return _cache[id]; | ||||
|   } | ||||
|  | ||||
|   Future<void> _saveToLocal(Iterable<SnAccount> out) async { | ||||
|     // For better on conflict resolution | ||||
|     // And consider the method usually called with usually small amount of data | ||||
|     // Use for to insert each record instead of bulk insert | ||||
|     List<Future<int>> queries = out.map((ele) { | ||||
|       return _dt.db.snLocalAccount.insertOne( | ||||
|         SnLocalAccountCompanion.insert( | ||||
|           id: Value(ele.id), | ||||
|           name: ele.name, | ||||
|           content: ele, | ||||
|           cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)), | ||||
|         ), | ||||
|         onConflict: DoUpdate( | ||||
|           (_) => SnLocalAccountCompanion.custom( | ||||
|             name: Constant(ele.name), | ||||
|             content: Constant(jsonEncode(ele.toJson())), | ||||
|             cacheExpiredAt: | ||||
|                 Constant(DateTime.now().add(const Duration(hours: 1))), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     }).toList(); | ||||
|     await Future.wait(queries); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
| @@ -30,8 +29,8 @@ class UserProvider extends ChangeNotifier { | ||||
|     notifyListeners(); | ||||
|     refreshUser().then((value) async { | ||||
|       if (value != null) { | ||||
|         log('Logged in as @${value.name}'); | ||||
|         log('Atk: ${await atk}'); | ||||
|         logging.info('[Auth] Logged in as @${value.name}'); | ||||
|         logging.debug('[Auth] Access token: ${await atk}'); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_udid/flutter_udid.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/websocket.dart'; | ||||
| import 'package:web_socket_channel/io.dart'; | ||||
| import 'package:web_socket_channel/web_socket_channel.dart'; | ||||
|  | ||||
| class WebSocketProvider extends ChangeNotifier { | ||||
| @@ -30,7 +33,7 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|     if (isConnected) return; | ||||
|     if (!_ua.isAuthorized) return; | ||||
|  | ||||
|     log('[WebSocket] Connecting to the server...'); | ||||
|     logging.debug('[WebSocket] Connecting to the server...'); | ||||
|     await connect(); | ||||
|   } | ||||
|  | ||||
| @@ -39,7 +42,7 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|   Future<void> connect({noRetry = false}) async { | ||||
|     if (_connectCompleter != null) { | ||||
|       await _connectCompleter!.future; | ||||
|       _connectCompleter = null; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (!_ua.isAuthorized) return; | ||||
| @@ -52,27 +55,39 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|  | ||||
|       final atk = await _sn.getFreshAtk(); | ||||
|       final uri = Uri.parse( | ||||
|         '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk', | ||||
|         kIsWeb | ||||
|             ? '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk' | ||||
|             : '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?clientId=${await FlutterUdid.consistentUdid}tk=$atk', | ||||
|       ); | ||||
|  | ||||
|       isBusy = true; | ||||
|       notifyListeners(); | ||||
|  | ||||
|       conn = WebSocketChannel.connect(uri); | ||||
|       conn = kIsWeb | ||||
|           ? WebSocketChannel.connect(uri) | ||||
|           : IOWebSocketChannel.connect( | ||||
|               uri, | ||||
|               headers: {'Authorization': 'Bearer $atk'}, | ||||
|             ); | ||||
|       await conn!.ready; | ||||
|       _wsStream = conn!.stream.asBroadcastStream(); | ||||
|       listen(); | ||||
|       log('[WebSocket] Connected to server!'); | ||||
|       logging.info('[WebSocket] Connected to server!'); | ||||
|       isConnected = true; | ||||
|     } catch (err) { | ||||
|       if (err is WebSocketChannelException) { | ||||
|         log('Failed to connect to websocket: ${(err.inner as dynamic).message}'); | ||||
|         logging.error( | ||||
|           '[WebSocket] Failed to connect to websocket...', | ||||
|           err.inner, | ||||
|         ); | ||||
|       } else { | ||||
|         log('Failed to connect to websocket: $err'); | ||||
|         logging.error('[WebSocket] Failed to connect to websocket...', err); | ||||
|       } | ||||
|  | ||||
|       if (!noRetry) { | ||||
|         log('Retry connecting to websocket in 3 seconds...'); | ||||
|         logging.warning( | ||||
|           '[WebSocket] Retry connecting to websocket in 3 seconds...', | ||||
|         ); | ||||
|         return Future.delayed( | ||||
|           const Duration(seconds: 3), | ||||
|           () => connect(noRetry: true), | ||||
| @@ -100,7 +115,9 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|     _wsStream!.listen( | ||||
|       (event) { | ||||
|         final packet = WebSocketPackage.fromJson(jsonDecode(event)); | ||||
|         log('Websocket incoming message: ${packet.method} ${packet.message}'); | ||||
|         logging.debug( | ||||
|           '[Websocket] Incoming message: ${packet.method} ${packet.message}', | ||||
|         ); | ||||
|         pk.sink.add(packet); | ||||
|       }, | ||||
|       onDone: () { | ||||
|   | ||||
							
								
								
									
										117
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								lib/router.dart
									
									
									
									
									
								
							| @@ -4,7 +4,9 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:surface/screens/abuse_report.dart'; | ||||
| import 'package:surface/screens/account.dart'; | ||||
| import 'package:surface/screens/account/account_settings.dart'; | ||||
| import 'package:surface/screens/account/badges.dart'; | ||||
| import 'package:surface/screens/account/factor_settings.dart'; | ||||
| import 'package:surface/screens/account/keypairs.dart'; | ||||
| import 'package:surface/screens/account/profile_page.dart'; | ||||
| import 'package:surface/screens/account/profile_edit.dart'; | ||||
| import 'package:surface/screens/account/publishers/publisher_edit.dart'; | ||||
| @@ -21,6 +23,7 @@ import 'package:surface/screens/chat/room.dart'; | ||||
| import 'package:surface/screens/explore.dart'; | ||||
| import 'package:surface/screens/friend.dart'; | ||||
| import 'package:surface/screens/home.dart'; | ||||
| import 'package:surface/screens/logging.dart'; | ||||
| import 'package:surface/screens/news/news_detail.dart'; | ||||
| import 'package:surface/screens/news/news_list.dart'; | ||||
| import 'package:surface/screens/notification.dart'; | ||||
| @@ -63,10 +66,10 @@ final _appRoutes = [ | ||||
|     builder: (context, state) => const ExploreScreen(), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/write/:mode', | ||||
|         path: '/write', | ||||
|         name: 'postEditor', | ||||
|         builder: (context, state) => PostEditorScreen( | ||||
|           mode: state.pathParameters['mode']!, | ||||
|           mode: state.uri.queryParameters['mode'], | ||||
|           postEditId: int.tryParse( | ||||
|             state.uri.queryParameters['editing'] ?? '', | ||||
|           ), | ||||
| @@ -105,55 +108,66 @@ final _appRoutes = [ | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|       path: '/account', | ||||
|       name: 'account', | ||||
|       builder: (context, state) => const AccountScreen(), | ||||
|       routes: [ | ||||
|         GoRoute( | ||||
|           path: '/wallet', | ||||
|           name: 'accountWallet', | ||||
|           builder: (context, state) => const WalletScreen(), | ||||
|     path: '/account', | ||||
|     name: 'account', | ||||
|     builder: (context, state) => const AccountScreen(), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/badges', | ||||
|         name: 'accountBadges', | ||||
|         builder: (context, state) => const AccountBadgesScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/wallet', | ||||
|         name: 'accountWallet', | ||||
|         builder: (context, state) => const WalletScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/keypairs', | ||||
|         name: 'accountKeyPairs', | ||||
|         builder: (context, state) => const KeyPairScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/settings', | ||||
|         name: 'accountSettings', | ||||
|         builder: (context, state) => AccountSettingsScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/settings/factors', | ||||
|         name: 'factorSettings', | ||||
|         builder: (context, state) => FactorSettingsScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/profile/edit', | ||||
|         name: 'accountProfileEdit', | ||||
|         builder: (context, state) => ProfileEditScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/publishers', | ||||
|         name: 'accountPublishers', | ||||
|         builder: (context, state) => PublisherScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/publishers/new', | ||||
|         name: 'accountPublisherNew', | ||||
|         builder: (context, state) => AccountPublisherNewScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/publishers/edit/:name', | ||||
|         name: 'accountPublisherEdit', | ||||
|         builder: (context, state) => AccountPublisherEditScreen( | ||||
|           name: state.pathParameters['name']!, | ||||
|         ), | ||||
|         GoRoute( | ||||
|           path: '/settings', | ||||
|           name: 'accountSettings', | ||||
|           builder: (context, state) => AccountSettingsScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/:name', | ||||
|         name: 'accountProfilePage', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: UserScreen(name: state.pathParameters['name']!), | ||||
|         ), | ||||
|         GoRoute( | ||||
|           path: '/settings/factors', | ||||
|           name: 'factorSettings', | ||||
|           builder: (context, state) => FactorSettingsScreen(), | ||||
|         ), | ||||
|         GoRoute( | ||||
|           path: '/profile/edit', | ||||
|           name: 'accountProfileEdit', | ||||
|           builder: (context, state) => ProfileEditScreen(), | ||||
|         ), | ||||
|         GoRoute( | ||||
|           path: '/publishers', | ||||
|           name: 'accountPublishers', | ||||
|           builder: (context, state) => PublisherScreen(), | ||||
|         ), | ||||
|         GoRoute( | ||||
|           path: '/publishers/new', | ||||
|           name: 'accountPublisherNew', | ||||
|           builder: (context, state) => AccountPublisherNewScreen(), | ||||
|         ), | ||||
|         GoRoute( | ||||
|           path: '/publishers/edit/:name', | ||||
|           name: 'accountPublisherEdit', | ||||
|           builder: (context, state) => AccountPublisherEditScreen( | ||||
|             name: state.pathParameters['name']!, | ||||
|           ), | ||||
|         ), | ||||
|         GoRoute( | ||||
|           path: '/:name', | ||||
|           name: 'accountProfilePage', | ||||
|           pageBuilder: (context, state) => NoTransitionPage( | ||||
|             child: UserScreen(name: state.pathParameters['name']!), | ||||
|           ), | ||||
|         ), | ||||
|       ]), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/chat', | ||||
|     name: 'chat', | ||||
| @@ -249,6 +263,11 @@ final _appRoutes = [ | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/debug/logging', | ||||
|     name: 'debugLogging', | ||||
|     builder: (context, state) => const DebugLoggingScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/album', | ||||
|     name: 'album', | ||||
|   | ||||
| @@ -125,8 +125,14 @@ class _AuthorizedAccountScreen extends StatelessWidget { | ||||
|                           .textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                     ], | ||||
|                   ), | ||||
|                   Text(ua.user!.description) | ||||
|                       .textStyle(Theme.of(context).textTheme.bodyMedium!), | ||||
|                   Text( | ||||
|                     (ua.user!.profile?.description.isNotEmpty ?? false) | ||||
|                         ? ua.user!.profile!.description | ||||
|                         : 'userNoDescription'.tr(), | ||||
|                     style: (ua.user!.profile?.description.isEmpty ?? true) | ||||
|                         ? TextStyle(fontStyle: FontStyle.italic) | ||||
|                         : null, | ||||
|                   ).textStyle(Theme.of(context).textTheme.bodyMedium!), | ||||
|                 ], | ||||
|               ), | ||||
|             ); | ||||
| @@ -172,6 +178,26 @@ class _AuthorizedAccountScreen extends StatelessWidget { | ||||
|             GoRouter.of(context).pushNamed('accountWallet'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountBadges').tr(), | ||||
|           subtitle: Text('accountBadgesDescription').tr(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Symbols.award_star), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|           onTap: () { | ||||
|             GoRouter.of(context).pushNamed('accountBadges'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountKeyPairs').tr(), | ||||
|           subtitle: Text('accountKeyPairsDescription').tr(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Symbols.key), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|           onTap: () { | ||||
|             GoRouter.of(context).pushNamed('accountKeyPairs'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountSettings').tr(), | ||||
|           subtitle: Text('accountSettingsSubtitle').tr(), | ||||
|   | ||||
							
								
								
									
										140
									
								
								lib/screens/account/badges.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								lib/screens/account/badges.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/screens/account/profile_page.dart' show kBadgesMeta; | ||||
| import 'package:surface/theme.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class AccountBadgesScreen extends StatefulWidget { | ||||
|   const AccountBadgesScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<AccountBadgesScreen> createState() => _AccountBadgesScreenState(); | ||||
| } | ||||
|  | ||||
| class _AccountBadgesScreenState extends State<AccountBadgesScreen> { | ||||
|   bool _isBusy = false; | ||||
|   List<SnAccountBadge>? _badges; | ||||
|  | ||||
|   Future<void> _fetchBadges() async { | ||||
|     setState(() => _isBusy = true); | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/id/badges/me'); | ||||
|       if (!mounted) return; | ||||
|       setState( | ||||
|         () => _badges = List<SnAccountBadge>.from( | ||||
|           resp.data?.map((e) => SnAccountBadge.fromJson(e)) ?? [], | ||||
|         ), | ||||
|       ); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool _isActivating = false; | ||||
|  | ||||
|   Future<void> _activateBadge(SnAccountBadge badge) async { | ||||
|     try { | ||||
|       setState(() => _isActivating = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post('/cgi/id/badges/${badge.id}/active'); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('badgeActivated' | ||||
|           .tr(args: [(kBadgesMeta[badge.type]?.$1 ?? 'unknown').tr()])); | ||||
|       await _fetchBadges(); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isActivating = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchBadges(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text('screenAccountBadges').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           if (_badges != null) | ||||
|             Expanded( | ||||
|               child: MediaQuery.removePadding( | ||||
|                 context: context, | ||||
|                 removeTop: true, | ||||
|                 child: RefreshIndicator( | ||||
|                   onRefresh: _fetchBadges, | ||||
|                   child: ListView.builder( | ||||
|                     itemCount: _badges!.length, | ||||
|                     itemBuilder: (context, idx) { | ||||
|                       final badge = _badges![idx]; | ||||
|                       return ListTile( | ||||
|                         title: Text( | ||||
|                           kBadgesMeta[badge.type]?.$1 ?? 'unknown', | ||||
|                         ).tr(), | ||||
|                         contentPadding: const EdgeInsets.only( | ||||
|                           left: 24, | ||||
|                           right: 16, | ||||
|                           top: 4, | ||||
|                           bottom: 4, | ||||
|                         ), | ||||
|                         subtitle: Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             if (badge.metadata['title'] != null) | ||||
|                               Text(badge.metadata['title']).fontSize(14).bold() | ||||
|                             else | ||||
|                               Text( | ||||
|                                 '#${badge.id.toString().padLeft(8, '0')}', | ||||
|                                 style: GoogleFonts.robotoMono(), | ||||
|                               ).fontSize(14).bold(), | ||||
|                             Text( | ||||
|                               DateFormat('y/M/d').format(badge.createdAt), | ||||
|                             ) | ||||
|                           ], | ||||
|                         ), | ||||
|                         trailing: IconButton( | ||||
|                           icon: const Icon(Symbols.check), | ||||
|                           onPressed: (badge.isActive || _isActivating) | ||||
|                               ? null | ||||
|                               : () { | ||||
|                                   _activateBadge(badge); | ||||
|                                 }, | ||||
|                         ), | ||||
|                         leading: Icon( | ||||
|                           kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark, | ||||
|                           color: badge.metadata['color'] != null | ||||
|                               ? HexColor.fromHex(badge.metadata['color']!) | ||||
|                               : kBadgesMeta[badge.type]?.$3, | ||||
|                           fill: 1, | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										106
									
								
								lib/screens/account/keypairs.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								lib/screens/account/keypairs.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/keypair.dart'; | ||||
| import 'package:surface/types/keypair.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class KeyPairScreen extends StatefulWidget { | ||||
|   const KeyPairScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<KeyPairScreen> createState() => _KeyPairScreenState(); | ||||
| } | ||||
|  | ||||
| class _KeyPairScreenState extends State<KeyPairScreen> { | ||||
|   bool _isBusy = false; | ||||
|   List<SnKeyPair>? _keyPairs; | ||||
|  | ||||
|   Future<void> _loadKeyPairs() async { | ||||
|     setState(() => _isBusy = true); | ||||
|     final kps = await context.read<KeyPairProvider>().listKeyPair(); | ||||
|     setState(() { | ||||
|       _keyPairs = kps; | ||||
|       _isBusy = false; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _loadKeyPairs(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text('screenKeyPairs').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           ListTile( | ||||
|             leading: const Icon(Symbols.add), | ||||
|             title: Text('enrollNewKeyPair').tr(), | ||||
|             subtitle: Text('enrollNewKeyPairDescription').tr(), | ||||
|             onTap: () async { | ||||
|               await context.read<KeyPairProvider>().enrollNew(); | ||||
|               _loadKeyPairs(); | ||||
|             }, | ||||
|           ), | ||||
|           const Divider(height: 1), | ||||
|           if (_keyPairs != null) | ||||
|             Expanded( | ||||
|               child: MediaQuery.removePadding( | ||||
|                 context: context, | ||||
|                 removeTop: true, | ||||
|                 child: RefreshIndicator( | ||||
|                   onRefresh: _loadKeyPairs, | ||||
|                   child: ListView.builder( | ||||
|                     itemCount: _keyPairs!.length, | ||||
|                     itemBuilder: (context, index) { | ||||
|                       final kp = _keyPairs![index]; | ||||
|                       return ListTile( | ||||
|                         title: Text(kp.id.toUpperCase()), | ||||
|                         subtitle: Row( | ||||
|                           spacing: 8, | ||||
|                           children: [ | ||||
|                             if (kp.privateKey != null) | ||||
|                               Text( | ||||
|                                 'keyPairHasPrivateKey'.tr(), | ||||
|                               ), | ||||
|                             if (kp.privateKey != null) Text('·'), | ||||
|                             Flexible( | ||||
|                               flex: 1, | ||||
|                               child: Text( | ||||
|                                 'UID #${kp.accountId.toString().padLeft(8, '0')}', | ||||
|                                 style: GoogleFonts.robotoMono(), | ||||
|                               ), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                         trailing: IconButton( | ||||
|                           icon: const Icon(Symbols.check), | ||||
|                           onPressed: kp.isActive == true | ||||
|                               ? null | ||||
|                               : () async { | ||||
|                                   final k = context.read<KeyPairProvider>(); | ||||
|                                   await k.activeKeyPair(kp.id); | ||||
|                                   _loadKeyPairs(); | ||||
|                                 }, | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/cupertino.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_timezone/flutter_timezone.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| @@ -36,11 +37,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|   final _firstNameController = TextEditingController(); | ||||
|   final _lastNameController = TextEditingController(); | ||||
|   final _descriptionController = TextEditingController(); | ||||
|   final _timezoneController = TextEditingController(); | ||||
|   final _genderController = TextEditingController(); | ||||
|   final _pronounsController = TextEditingController(); | ||||
|   final _locationController = TextEditingController(); | ||||
|   final _birthdayController = TextEditingController(); | ||||
|  | ||||
|   String? _avatar; | ||||
|   String? _banner; | ||||
|   DateTime? _birthday; | ||||
|   List<(String, String)>? _links; | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|  | ||||
| @@ -51,43 +57,46 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|     final prof = ua.user!; | ||||
|     _usernameController.text = prof.name; | ||||
|     _nicknameController.text = prof.nick; | ||||
|     _descriptionController.text = prof.description; | ||||
|     _descriptionController.text = prof.profile!.description; | ||||
|     _firstNameController.text = prof.profile!.firstName; | ||||
|     _lastNameController.text = prof.profile!.lastName; | ||||
|     _timezoneController.text = prof.profile!.timeZone; | ||||
|     _genderController.text = prof.profile!.gender; | ||||
|     _pronounsController.text = prof.profile!.pronouns; | ||||
|     _locationController.text = prof.profile!.location; | ||||
|     _avatar = prof.avatar; | ||||
|     _banner = prof.banner; | ||||
|     if (prof.profile!.birthday != null) { | ||||
|       _birthdayController.text = DateFormat(_kDateFormat).format( | ||||
|         prof.profile!.birthday!.toLocal(), | ||||
|       ); | ||||
|     _links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList(); | ||||
|     _birthday = prof.profile!.birthday?.toLocal(); | ||||
|     if (_birthday != null) { | ||||
|       _birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _selectBirthday() async { | ||||
|     await showCupertinoModalPopup<DateTime?>( | ||||
|       context: context, | ||||
|       builder: (BuildContext context) => Container( | ||||
|         height: 216, | ||||
|         padding: const EdgeInsets.only(top: 6.0), | ||||
|         margin: EdgeInsets.only( | ||||
|           bottom: MediaQuery.of(context).viewInsets.bottom, | ||||
|         ), | ||||
|         color: Theme.of(context).colorScheme.surface, | ||||
|         child: SafeArea( | ||||
|           top: false, | ||||
|           child: CupertinoDatePicker( | ||||
|             initialDateTime: _birthday?.toLocal(), | ||||
|             mode: CupertinoDatePickerMode.date, | ||||
|             use24hFormat: true, | ||||
|             onDateTimeChanged: (DateTime newDate) { | ||||
|               setState(() { | ||||
|                 _birthday = newDate; | ||||
|                 _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); | ||||
|               }); | ||||
|             }, | ||||
|       builder: | ||||
|           (BuildContext context) => Container( | ||||
|             height: 216, | ||||
|             padding: const EdgeInsets.only(top: 6.0), | ||||
|             margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), | ||||
|             color: Theme.of(context).colorScheme.surface, | ||||
|             child: SafeArea( | ||||
|               top: false, | ||||
|               child: CupertinoDatePicker( | ||||
|                 initialDateTime: _birthday?.toLocal(), | ||||
|                 mode: CupertinoDatePickerMode.date, | ||||
|                 use24hFormat: true, | ||||
|                 onDateTimeChanged: (DateTime newDate) { | ||||
|                   setState(() { | ||||
|                     _birthday = newDate; | ||||
|                     _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -96,32 +105,42 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|     if (image == null) return; | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); | ||||
|     final aspectRatios = | ||||
|         place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; | ||||
|     final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||
|         ? await showCupertinoImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             allowedAspectRatios: aspectRatios, | ||||
|             imageProvider: imageProvider, | ||||
|           ) | ||||
|         : await showMaterialImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             allowedAspectRatios: aspectRatios, | ||||
|             imageProvider: imageProvider, | ||||
|           ); | ||||
|     final skipCrop = image.path.endsWith('.gif'); | ||||
|  | ||||
|     if (result == null) return; | ||||
|     Uint8List? rawBytes; | ||||
|     if (!skipCrop) { | ||||
|       final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); | ||||
|       final aspectRatios = | ||||
|           place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; | ||||
|       final result = | ||||
|           (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||
|               ? await showCupertinoImageCropper( | ||||
|                 // ignore: use_build_context_synchronously | ||||
|                 context, | ||||
|                 allowedAspectRatios: aspectRatios, | ||||
|                 imageProvider: imageProvider, | ||||
|               ) | ||||
|               : await showMaterialImageCropper( | ||||
|                 // ignore: use_build_context_synchronously | ||||
|                 context, | ||||
|                 allowedAspectRatios: aspectRatios, | ||||
|                 imageProvider: imageProvider, | ||||
|               ); | ||||
|  | ||||
|       if (result == null) return; | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       setState(() => _isBusy = true); | ||||
|       rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); | ||||
|     } else { | ||||
|       if (!mounted) return; | ||||
|       setState(() => _isBusy = true); | ||||
|       rawBytes = await image.readAsBytes(); | ||||
|     } | ||||
|  | ||||
|     if (!mounted) return; | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); | ||||
|  | ||||
|     try { | ||||
|       final attachment = await attach.directUploadOne( | ||||
|         rawBytes, | ||||
| @@ -133,10 +152,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.put( | ||||
|         '/cgi/id/users/me/$place', | ||||
|         data: {'attachment': attachment.rid}, | ||||
|       ); | ||||
|       await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid}); | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       final ua = context.read<UserProvider>(); | ||||
| @@ -166,7 +182,14 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|           'description': _descriptionController.value.text, | ||||
|           'first_name': _firstNameController.value.text, | ||||
|           'last_name': _lastNameController.value.text, | ||||
|           'time_zone': _timezoneController.value.text, | ||||
|           'gender': _genderController.value.text, | ||||
|           'pronouns': _pronounsController.value.text, | ||||
|           'location': _locationController.value.text, | ||||
|           'birthday': _birthday?.toUtc().toIso8601String(), | ||||
|           'links': { | ||||
|             for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2, | ||||
|           }, | ||||
|         }, | ||||
|       ); | ||||
|  | ||||
| @@ -197,6 +220,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|     _firstNameController.dispose(); | ||||
|     _lastNameController.dispose(); | ||||
|     _descriptionController.dispose(); | ||||
|     _timezoneController.dispose(); | ||||
|     _genderController.dispose(); | ||||
|     _pronounsController.dispose(); | ||||
|     _locationController.dispose(); | ||||
|     _birthdayController.dispose(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| @@ -208,10 +235,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAccountProfileEdit').tr(), | ||||
|       ), | ||||
|       appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
| @@ -230,12 +254,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|                         aspectRatio: 16 / 9, | ||||
|                         child: Container( | ||||
|                           color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                           child: _banner != null | ||||
|                               ? AutoResizeUniversalImage( | ||||
|                                   sn.getAttachmentUrl(_banner!), | ||||
|                                   fit: BoxFit.cover, | ||||
|                                 ) | ||||
|                               : const SizedBox.shrink(), | ||||
|                           child: | ||||
|                               _banner != null | ||||
|                                   ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) | ||||
|                                   : const SizedBox.shrink(), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
| @@ -262,6 +284,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|             ).padding(horizontal: padding), | ||||
|             const Gap(8 + 28), | ||||
|             Column( | ||||
|               spacing: 4, | ||||
|               children: [ | ||||
|                 TextField( | ||||
|                   readOnly: true, | ||||
| @@ -271,16 +294,13 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|                     labelText: 'fieldUsername'.tr(), | ||||
|                     helperText: 'fieldUsernameCannotEditHint'.tr(), | ||||
|                   ), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 TextField( | ||||
|                   controller: _nicknameController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     labelText: 'fieldNickname'.tr(), | ||||
|                   ), | ||||
|                   decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     Flexible( | ||||
| @@ -291,6 +311,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|                           border: const UnderlineInputBorder(), | ||||
|                           labelText: 'fieldFirstName'.tr(), | ||||
|                         ), | ||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Gap(8), | ||||
| @@ -302,31 +323,165 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|                           border: const UnderlineInputBorder(), | ||||
|                           labelText: 'fieldLastName'.tr(), | ||||
|                         ), | ||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     Flexible( | ||||
|                       flex: 1, | ||||
|                       child: TextField( | ||||
|                         controller: _genderController, | ||||
|                         decoration: InputDecoration( | ||||
|                           border: const UnderlineInputBorder(), | ||||
|                           labelText: 'fieldGender'.tr(), | ||||
|                         ), | ||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Gap(4), | ||||
|                     Flexible( | ||||
|                       flex: 1, | ||||
|                       child: TextField( | ||||
|                         controller: _pronounsController, | ||||
|                         decoration: InputDecoration( | ||||
|                           border: const UnderlineInputBorder(), | ||||
|                           labelText: 'fieldPronouns'.tr(), | ||||
|                         ), | ||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 TextField( | ||||
|                   controller: _descriptionController, | ||||
|                   keyboardType: TextInputType.multiline, | ||||
|                   maxLines: null, | ||||
|                   minLines: 3, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     labelText: 'fieldDescription'.tr(), | ||||
|                   ), | ||||
|                   decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|                 Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     Expanded( | ||||
|                       child: TextField( | ||||
|                         controller: _timezoneController, | ||||
|                         decoration: InputDecoration( | ||||
|                           border: const UnderlineInputBorder(), | ||||
|                           labelText: 'fieldTimeZone'.tr(), | ||||
|                         ), | ||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Gap(4), | ||||
|                     StyledWidget( | ||||
|                       IconButton( | ||||
|                         icon: const Icon(Symbols.calendar_month), | ||||
|                         visualDensity: VisualDensity(horizontal: -4, vertical: -4), | ||||
|                         padding: EdgeInsets.zero, | ||||
|                         constraints: const BoxConstraints(), | ||||
|                         onPressed: () async { | ||||
|                           _timezoneController.text = await FlutterTimezone.getLocalTimezone(); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ).padding(top: 6), | ||||
|                     const Gap(4), | ||||
|                     StyledWidget( | ||||
|                       IconButton( | ||||
|                         icon: const Icon(Symbols.clear), | ||||
|                         visualDensity: VisualDensity(horizontal: -4, vertical: -4), | ||||
|                         padding: EdgeInsets.zero, | ||||
|                         constraints: const BoxConstraints(), | ||||
|                         onPressed: () { | ||||
|                           _timezoneController.clear(); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ).padding(top: 6), | ||||
|                   ], | ||||
|                 ), | ||||
|                 TextField( | ||||
|                   controller: _locationController, | ||||
|                   decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 TextField( | ||||
|                   controller: _birthdayController, | ||||
|                   readOnly: true, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     labelText: 'fieldBirthday'.tr(), | ||||
|                   ), | ||||
|                   decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()), | ||||
|                   onTap: () => _selectBirthday(), | ||||
|                 ), | ||||
|                 if (_links != null) | ||||
|                   Card( | ||||
|                     margin: const EdgeInsets.only(top: 16, bottom: 4), | ||||
|                     child: Container( | ||||
|                       width: double.infinity, | ||||
|                       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|                       child: Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Row( | ||||
|                             children: [ | ||||
|                               Expanded( | ||||
|                                 child: Text( | ||||
|                                   'fieldLinks'.tr(), | ||||
|                                   style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17), | ||||
|                                 ), | ||||
|                               ), | ||||
|                               IconButton( | ||||
|                                 padding: EdgeInsets.zero, | ||||
|                                 constraints: const BoxConstraints(), | ||||
|                                 visualDensity: VisualDensity(horizontal: -4, vertical: -4), | ||||
|                                 icon: const Icon(Symbols.add), | ||||
|                                 onPressed: () { | ||||
|                                   setState(() => _links!.add(('', ''))); | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           const Gap(8), | ||||
|                           for (var idx = 0; idx < _links!.length; idx++) | ||||
|                             Row( | ||||
|                               children: [ | ||||
|                                 Flexible( | ||||
|                                   flex: 1, | ||||
|                                   child: TextFormField( | ||||
|                                     initialValue: _links![idx].$1, | ||||
|                                     decoration: InputDecoration( | ||||
|                                       isDense: true, | ||||
|                                       border: const OutlineInputBorder(), | ||||
|                                       labelText: 'fieldLinkName'.tr(), | ||||
|                                     ), | ||||
|                                     onChanged: (value) { | ||||
|                                       _links![idx] = (value, _links![idx].$2); | ||||
|                                     }, | ||||
|                                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 const Gap(8), | ||||
|                                 Flexible( | ||||
|                                   flex: 1, | ||||
|                                   child: TextFormField( | ||||
|                                     initialValue: _links![idx].$2, | ||||
|                                     decoration: InputDecoration( | ||||
|                                       isDense: true, | ||||
|                                       border: const OutlineInputBorder(), | ||||
|                                       labelText: 'fieldLinkUrl'.tr(), | ||||
|                                     ), | ||||
|                                     onChanged: (value) { | ||||
|                                       _links![idx] = (_links![idx].$1, value); | ||||
|                                     }, | ||||
|                                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|               ], | ||||
|             ).padding(horizontal: padding + 8), | ||||
|             const Gap(12), | ||||
| @@ -340,6 +495,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|                 ), | ||||
|               ], | ||||
|             ).padding(horizontal: padding), | ||||
|             Gap(MediaQuery.of(context).padding.bottom), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -20,8 +20,10 @@ import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:surface/theme.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| const Map<String, (String, IconData, Color)> kBadgesMeta = { | ||||
| final Map<String, (String, IconData, Color)> kBadgesMeta = { | ||||
|   'company.staff': ( | ||||
|     'badgeCompanyStaff', | ||||
|     Symbols.tools_wrench, | ||||
| @@ -32,6 +34,31 @@ const Map<String, (String, IconData, Color)> kBadgesMeta = { | ||||
|     Symbols.flag, | ||||
|     Colors.orange, | ||||
|   ), | ||||
|   'site.anniversary': ( | ||||
|     'badgeSiteAnniversary', | ||||
|     Symbols.celebration, | ||||
|     Colors.orangeAccent, | ||||
|   ), | ||||
|   'user.birthday': ( | ||||
|     'badgeUserBirthday', | ||||
|     Symbols.cake, | ||||
|     Colors.red[400]!, | ||||
|   ), | ||||
|   'community.survey': ( | ||||
|     'badgeCommunitySurvey', | ||||
|     Symbols.star, | ||||
|     Colors.yellow[700]!, | ||||
|   ), | ||||
|   'community.verified': ( | ||||
|     'badgeCommunityVerified', | ||||
|     Symbols.verified, | ||||
|     Colors.blue, | ||||
|   ), | ||||
|   'community.contributor': ( | ||||
|     'badgeCommunityContributor', | ||||
|     Symbols.thumb_up, | ||||
|     Colors.lightGreen, | ||||
|   ), | ||||
| }; | ||||
|  | ||||
| class UserScreen extends StatefulWidget { | ||||
| @@ -43,7 +70,8 @@ class UserScreen extends StatefulWidget { | ||||
|   State<UserScreen> createState() => _UserScreenState(); | ||||
| } | ||||
|  | ||||
| class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin { | ||||
| class _UserScreenState extends State<UserScreen> | ||||
|     with SingleTickerProviderStateMixin { | ||||
|   late final ScrollController _scrollController = ScrollController(); | ||||
|  | ||||
|   SnAccount? _account; | ||||
| @@ -64,13 +92,18 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<List<SnCheckInRecord>> _getCheckInRecords() async { | ||||
|   List<SnCheckInRecord>? _records; | ||||
|  | ||||
|   Future<void> _getCheckInRecords() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14'); | ||||
|       return List.from( | ||||
|         resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [], | ||||
|       ); | ||||
|       final resp = | ||||
|           await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14'); | ||||
|       setState(() { | ||||
|         _records = List.from( | ||||
|           resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [], | ||||
|         ); | ||||
|       }); | ||||
|     } catch (err) { | ||||
|       if (mounted) context.showErrorDialog(err); | ||||
|       rethrow; | ||||
| @@ -98,7 +131,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|   Future<void> _fetchPublishers() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}'); | ||||
|       final resp = | ||||
|           await sn.client.get('/cgi/co/publishers?user=${widget.name}'); | ||||
|       _publishers = List<SnPublisher>.from( | ||||
|         resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], | ||||
|       ); | ||||
| @@ -144,7 +178,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|         'related': _account!.name, | ||||
|       }); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); | ||||
|       context.showSnackbar( | ||||
|           'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
| @@ -160,9 +195,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|  | ||||
|     try { | ||||
|       final rel = context.read<SnRelationshipProvider>(); | ||||
|       await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {}); | ||||
|       await rel.updateRelationship( | ||||
|           _account!.id, 1, _accountRelationship?.permNodes ?? {}); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); | ||||
|       context.showSnackbar( | ||||
|           'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
| @@ -188,12 +225,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|   double _appBarBlur = 0.0; | ||||
|  | ||||
|   late final _appBarWidth = MediaQuery.of(context).size.width; | ||||
|   late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble(); | ||||
|   late final _appBarHeight = | ||||
|       (_appBarWidth * kBannerAspectRatio).roundToDouble(); | ||||
|  | ||||
|   void _updateAppBarBlur() { | ||||
|     if (_scrollController.offset > _appBarHeight) return; | ||||
|     setState(() { | ||||
|       _appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); | ||||
|       _appBarBlur = | ||||
|           (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -205,6 +244,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|  | ||||
|       _fetchStatus(); | ||||
|       _fetchPublishers(); | ||||
|       _getCheckInRecords(); | ||||
|  | ||||
|       try { | ||||
|         final rel = context.read<SnRelationshipProvider>(); | ||||
| @@ -260,18 +300,20 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                       text: TextSpan(children: [ | ||||
|                         TextSpan( | ||||
|                           text: _account!.nick, | ||||
|                           style: Theme.of(context).textTheme.titleLarge!.copyWith( | ||||
|                                 color: Colors.white, | ||||
|                                 shadows: labelShadows, | ||||
|                               ), | ||||
|                           style: | ||||
|                               Theme.of(context).textTheme.titleLarge!.copyWith( | ||||
|                                     color: Colors.white, | ||||
|                                     shadows: labelShadows, | ||||
|                                   ), | ||||
|                         ), | ||||
|                         const TextSpan(text: '\n'), | ||||
|                         TextSpan( | ||||
|                           text: '@${_account!.name}', | ||||
|                           style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                                 color: Colors.white, | ||||
|                                 shadows: labelShadows, | ||||
|                               ), | ||||
|                           style: | ||||
|                               Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                                     color: Colors.white, | ||||
|                                     shadows: labelShadows, | ||||
|                                   ), | ||||
|                         ), | ||||
|                       ]), | ||||
|                     ), | ||||
| @@ -280,14 +322,21 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                   ? Stack( | ||||
|                       fit: StackFit.expand, | ||||
|                       children: [ | ||||
|                         UniversalImage( | ||||
|                           sn.getAttachmentUrl(_account!.banner), | ||||
|                           fit: BoxFit.cover, | ||||
|                           height: imageHeight, | ||||
|                           width: _appBarWidth, | ||||
|                           cacheHeight: imageHeight, | ||||
|                           cacheWidth: _appBarWidth, | ||||
|                         ), | ||||
|                         if (_account!.banner.isNotEmpty) | ||||
|                           UniversalImage( | ||||
|                             sn.getAttachmentUrl(_account!.banner), | ||||
|                             fit: BoxFit.cover, | ||||
|                             height: imageHeight, | ||||
|                             width: _appBarWidth, | ||||
|                             cacheHeight: imageHeight, | ||||
|                             cacheWidth: _appBarWidth, | ||||
|                           ) | ||||
|                         else | ||||
|                           Container( | ||||
|                             color: Theme.of(context) | ||||
|                                 .colorScheme | ||||
|                                 .surfaceContainerHigh, | ||||
|                           ), | ||||
|                         Positioned( | ||||
|                           top: 0, | ||||
|                           left: 0, | ||||
| @@ -339,7 +388,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                       PopupMenuButton( | ||||
|                         padding: EdgeInsets.zero, | ||||
|                         style: ButtonStyle( | ||||
|                           visualDensity: VisualDensity(horizontal: -4, vertical: -4), | ||||
|                           visualDensity: | ||||
|                               VisualDensity(horizontal: -4, vertical: -4), | ||||
|                         ), | ||||
|                         itemBuilder: (context) => [ | ||||
|                           PopupMenuItem( | ||||
| @@ -389,8 +439,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).padding(right: 8), | ||||
|                   const Gap(12), | ||||
|                   Text(_account!.description).padding(horizontal: 8), | ||||
|                   if (_account!.profile!.description.isNotEmpty) | ||||
|                     const Gap(12) | ||||
|                   else | ||||
|                     const Gap(8), | ||||
|                   if (_account!.profile!.description.isNotEmpty) | ||||
|                     Text(_account!.profile!.description).padding(horizontal: 8), | ||||
|                   const Gap(4), | ||||
|                   Card( | ||||
|                     child: Row( | ||||
| @@ -399,7 +453,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                           Symbols.circle, | ||||
|                           fill: 1, | ||||
|                           size: 16, | ||||
|                           color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey, | ||||
|                           color: (_status?.isOnline ?? false) | ||||
|                               ? Colors.green | ||||
|                               : Colors.grey, | ||||
|                         ).padding(all: 4), | ||||
|                         const Gap(8), | ||||
|                         Text( | ||||
| @@ -409,7 +465,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                                   : 'accountStatusOffline'.tr() | ||||
|                               : 'loading'.tr(), | ||||
|                         ), | ||||
|                         if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null) | ||||
|                         if (_status != null && | ||||
|                             !_status!.isOnline && | ||||
|                             _status!.lastSeenAt != null) | ||||
|                           Text( | ||||
|                             'accountStatusLastSeen'.tr(args: [ | ||||
|                               _status!.lastSeenAt != null | ||||
| @@ -429,11 +487,15 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                           (ele) => Tooltip( | ||||
|                             richMessage: TextSpan( | ||||
|                               children: [ | ||||
|                                 TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), | ||||
|                                 TextSpan( | ||||
|                                   text: kBadgesMeta[ele.type]?.$1.tr() ?? | ||||
|                                       'unknown'.tr(), | ||||
|                                 ), | ||||
|                                 if (ele.metadata['title'] != null) | ||||
|                                   TextSpan( | ||||
|                                     text: '\n${ele.metadata['title']}', | ||||
|                                     style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                                     style: const TextStyle( | ||||
|                                         fontWeight: FontWeight.bold), | ||||
|                                   ), | ||||
|                                 TextSpan(text: '\n'), | ||||
|                                 TextSpan( | ||||
| @@ -442,8 +504,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                               ], | ||||
|                             ), | ||||
|                             child: Icon( | ||||
|                               kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, | ||||
|                               color: kBadgesMeta[ele.type]?.$3, | ||||
|                               kBadgesMeta[ele.type]?.$2 ?? | ||||
|                                   Symbols.question_mark, | ||||
|                               color: ele.metadata['color'] != null | ||||
|                                   ? HexColor.fromHex(ele.metadata['color']!) | ||||
|                                   : kBadgesMeta[ele.type]?.$3, | ||||
|                               fill: 1, | ||||
|                             ), | ||||
|                           ), | ||||
| @@ -458,7 +523,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.calendar_add_on), | ||||
|                           const Gap(8), | ||||
|                           Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]), | ||||
|                           Text('publisherJoinedAt').tr(args: [ | ||||
|                             DateFormat('y/M/d').format(_account!.createdAt) | ||||
|                           ]), | ||||
|                         ], | ||||
|                       ), | ||||
|                       Row( | ||||
| @@ -475,6 +542,44 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                           ]), | ||||
|                         ], | ||||
|                       ), | ||||
|                       if (_account!.profile!.gender.isNotEmpty || | ||||
|                           _account!.profile!.pronouns.isNotEmpty) | ||||
|                         Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                           children: [ | ||||
|                             const Icon(Symbols.wc), | ||||
|                             const Gap(8), | ||||
|                             Text( | ||||
|                               _account!.profile!.gender.isNotEmpty | ||||
|                                   ? _account!.profile!.gender | ||||
|                                   : 'unknown'.tr(), | ||||
|                             ), | ||||
|                             Text(' · ').padding(horizontal: 4), | ||||
|                             Text( | ||||
|                               _account!.profile!.pronouns.isNotEmpty | ||||
|                                   ? _account!.profile!.pronouns | ||||
|                                   : 'unknown'.tr(), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       if (_account!.profile!.timeZone.isNotEmpty) | ||||
|                         Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                           children: [ | ||||
|                             const Icon(Symbols.schedule), | ||||
|                             const Gap(8), | ||||
|                             Text(_account!.profile!.timeZone), | ||||
|                           ], | ||||
|                         ), | ||||
|                       if (_account!.profile!.location.isNotEmpty) | ||||
|                         Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                           children: [ | ||||
|                             const Icon(Symbols.location_on), | ||||
|                             const Gap(8), | ||||
|                             Text(_account!.profile!.location), | ||||
|                           ], | ||||
|                         ), | ||||
|                       Row( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
| @@ -491,17 +596,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.star), | ||||
|                           const Gap(8), | ||||
|                           Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'), | ||||
|                           Text( | ||||
|                               'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'), | ||||
|                           const Gap(8), | ||||
|                           Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5), | ||||
|                           Text(calcLevelUpProgressLevel( | ||||
|                                   _account?.profile?.experience ?? 0)) | ||||
|                               .fontSize(11) | ||||
|                               .opacity(0.5), | ||||
|                           const Gap(8), | ||||
|                           Container( | ||||
|                             width: double.infinity, | ||||
|                             constraints: const BoxConstraints(maxWidth: 160), | ||||
|                             child: LinearProgressIndicator( | ||||
|                               value: calcLevelUpProgress(_account?.profile?.experience ?? 0), | ||||
|                               value: calcLevelUpProgress( | ||||
|                                   _account?.profile?.experience ?? 0), | ||||
|                               borderRadius: BorderRadius.circular(8), | ||||
|                               backgroundColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                               backgroundColor: Theme.of(context) | ||||
|                                   .colorScheme | ||||
|                                   .surfaceContainer, | ||||
|                             ).alignment(Alignment.centerLeft), | ||||
|                           ), | ||||
|                         ], | ||||
| @@ -511,24 +623,46 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                 ], | ||||
|               ).padding(all: 16), | ||||
|             ), | ||||
|           if (_account?.profile?.links.isNotEmpty ?? false) | ||||
|             SliverToBoxAdapter(child: const Divider()), | ||||
|           if (_account?.profile?.links.isNotEmpty ?? false) | ||||
|             SliverToBoxAdapter( | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: _account!.profile!.links.entries.map((ele) { | ||||
|                   return ListTile( | ||||
|                     leading: const Icon(Symbols.link), | ||||
|                     title: Text(ele.key), | ||||
|                     subtitle: Text(ele.value), | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                     onTap: () { | ||||
|                       launchUrlString(ele.value); | ||||
|                     }, | ||||
|                   ); | ||||
|                 }).toList(), | ||||
|               ), | ||||
|             ), | ||||
|           SliverToBoxAdapter(child: const Divider()), | ||||
|           const SliverGap(12), | ||||
|           SliverToBoxAdapter( | ||||
|             child: FutureBuilder<List<SnCheckInRecord>>( | ||||
|               future: _getCheckInRecords(), | ||||
|               builder: (context, snapshot) { | ||||
|                 if (!snapshot.hasData) return const SizedBox.shrink(); | ||||
|                 if (snapshot.data!.length <= 1) { | ||||
|             child: Builder( | ||||
|               builder: (context) { | ||||
|                 if (_records == null) return const SizedBox.shrink(); | ||||
|                 if (_records!.length <= 1) { | ||||
|                   return Text( | ||||
|                     'accountCheckInNoRecords', | ||||
|                     textAlign: TextAlign.center, | ||||
|                   ).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8); | ||||
|                   ) | ||||
|                       .tr() | ||||
|                       .fontWeight(FontWeight.bold) | ||||
|                       .center() | ||||
|                       .padding(horizontal: 20, vertical: 8); | ||||
|                 } | ||||
|                 final records = snapshot.data!; | ||||
|                 return SizedBox( | ||||
|                   width: double.infinity, | ||||
|                   height: 240, | ||||
|                   child: CheckInRecordChart(records: records), | ||||
|                   child: CheckInRecordChart(records: _records!), | ||||
|                 ).padding( | ||||
|                   right: 24, | ||||
|                   left: 16, | ||||
| @@ -540,45 +674,55 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|           const SliverGap(12), | ||||
|           SliverToBoxAdapter(child: const Divider()), | ||||
|           const SliverGap(12), | ||||
|           SliverToBoxAdapter( | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                 SizedBox( | ||||
|                   height: 80, | ||||
|                   width: double.infinity, | ||||
|                   child: ListView( | ||||
|                     padding: EdgeInsets.symmetric(horizontal: 8), | ||||
|                     scrollDirection: Axis.horizontal, | ||||
|                     children: [ | ||||
|                       for (final badge in _account?.badges ?? []) | ||||
|                         SizedBox( | ||||
|                           width: 280, | ||||
|                           child: Card( | ||||
|                             child: ListTile( | ||||
|                               leading: Icon( | ||||
|                                 kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark, | ||||
|                                 color: kBadgesMeta[badge.type]?.$3, | ||||
|                                 fill: 1, | ||||
|           if (_account?.badges.isNotEmpty ?? false) | ||||
|             SliverToBoxAdapter( | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   Text('accountBadge') | ||||
|                       .bold() | ||||
|                       .fontSize(17) | ||||
|                       .tr() | ||||
|                       .padding(horizontal: 20, bottom: 4), | ||||
|                   SizedBox( | ||||
|                     height: 80, | ||||
|                     width: double.infinity, | ||||
|                     child: ListView( | ||||
|                       padding: EdgeInsets.symmetric(horizontal: 8), | ||||
|                       scrollDirection: Axis.horizontal, | ||||
|                       children: [ | ||||
|                         for (final badge in _account?.badges ?? []) | ||||
|                           SizedBox( | ||||
|                             width: 280, | ||||
|                             child: Card( | ||||
|                               child: ListTile( | ||||
|                                 leading: Icon( | ||||
|                                   kBadgesMeta[badge.type]?.$2 ?? | ||||
|                                       Symbols.question_mark, | ||||
|                                   color: badge.metadata['color'] != null | ||||
|                                       ? HexColor.fromHex( | ||||
|                                           badge.metadata['color']!) | ||||
|                                       : kBadgesMeta[badge.type]?.$3, | ||||
|                                   fill: 1, | ||||
|                                 ), | ||||
|                                 title: Text( | ||||
|                                   kBadgesMeta[badge.type]?.$1 ?? 'unknown', | ||||
|                                 ).tr(), | ||||
|                                 subtitle: badge.metadata['title'] != null | ||||
|                                     ? Text(badge.metadata['title']) | ||||
|                                     : Text( | ||||
|                                         DateFormat('y/M/d') | ||||
|                                             .format(badge.createdAt), | ||||
|                                       ), | ||||
|                               ), | ||||
|                               title: Text( | ||||
|                                 kBadgesMeta[badge.type]?.$1 ?? 'unknown', | ||||
|                               ).tr(), | ||||
|                               subtitle: badge.metadata['title'] != null | ||||
|                                   ? Text(badge.metadata['title']) | ||||
|                                   : Text( | ||||
|                                       DateFormat('y/M/d').format(badge.createdAt), | ||||
|                                     ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                     ], | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           const SliverGap(8), | ||||
|           SliverToBoxAdapter(child: const Divider()), | ||||
|           SliverList.builder( | ||||
| @@ -664,7 +808,8 @@ class CheckInRecordChart extends StatelessWidget { | ||||
|                   ), | ||||
|                 ) | ||||
|                 .toList(), | ||||
|             getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|             getTooltipColor: (_) => | ||||
|                 Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|           ), | ||||
|         ), | ||||
|         titlesData: FlTitlesData( | ||||
|   | ||||
| @@ -68,16 +68,19 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       await sn.client.put('/cgi/co/publishers/${widget.name}', data: { | ||||
|         'avatar': _avatar, | ||||
|         'banner': _banner, | ||||
|         'nick': _nickController.text, | ||||
|         'name': _nameController.text, | ||||
|         'description': _descriptionController.text, | ||||
|       }); | ||||
|       await sn.client.put( | ||||
|         '/cgi/co/publishers/${widget.name}', | ||||
|         data: { | ||||
|           'avatar': _avatar, | ||||
|           'banner': _banner, | ||||
|           'nick': _nickController.text, | ||||
|           'name': _nameController.text, | ||||
|           'description': _descriptionController.text, | ||||
|         }, | ||||
|       ); | ||||
|       if (mounted) Navigator.pop(context, true); | ||||
|     } catch (err) { | ||||
|       if(mounted) context.showErrorDialog(err); | ||||
|       if (mounted) context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
| @@ -97,7 +100,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|     _banner = ua.user!.banner; | ||||
|     _nickController.text = ua.user!.nick; | ||||
|     _nameController.text = ua.user!.name; | ||||
|     _descriptionController.text = ua.user!.description; | ||||
|     _descriptionController.text = ua.user!.profile!.description; | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
| @@ -108,32 +111,42 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|     if (image == null) return; | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); | ||||
|     final aspectRatios = | ||||
|         place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; | ||||
|     final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||
|         ? await showCupertinoImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             allowedAspectRatios: aspectRatios, | ||||
|             imageProvider: imageProvider, | ||||
|           ) | ||||
|         : await showMaterialImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             allowedAspectRatios: aspectRatios, | ||||
|             imageProvider: imageProvider, | ||||
|           ); | ||||
|     final skipCrop = image.path.endsWith('.gif'); | ||||
|  | ||||
|     if (result == null) return; | ||||
|     Uint8List? rawBytes; | ||||
|     if (!skipCrop) { | ||||
|       final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); | ||||
|       final aspectRatios = | ||||
|           place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; | ||||
|       final result = | ||||
|           (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||
|               ? await showCupertinoImageCropper( | ||||
|                 // ignore: use_build_context_synchronously | ||||
|                 context, | ||||
|                 allowedAspectRatios: aspectRatios, | ||||
|                 imageProvider: imageProvider, | ||||
|               ) | ||||
|               : await showMaterialImageCropper( | ||||
|                 // ignore: use_build_context_synchronously | ||||
|                 context, | ||||
|                 allowedAspectRatios: aspectRatios, | ||||
|                 imageProvider: imageProvider, | ||||
|               ); | ||||
|  | ||||
|       if (result == null) return; | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       setState(() => _isBusy = true); | ||||
|       rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); | ||||
|     } else { | ||||
|       if (!mounted) return; | ||||
|       setState(() => _isBusy = true); | ||||
|       rawBytes = await image.readAsBytes(); | ||||
|     } | ||||
|  | ||||
|     if (!mounted) return; | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); | ||||
|  | ||||
|     try { | ||||
|       final attachment = await attach.directUploadOne( | ||||
|         rawBytes, | ||||
| @@ -178,10 +191,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: PageBackButton(), | ||||
|         title: Text('screenAccountPublisherEdit').tr(), | ||||
|       ), | ||||
|       appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           children: [ | ||||
| @@ -199,12 +209,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|                         aspectRatio: 16 / 9, | ||||
|                         child: Container( | ||||
|                           color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                           child: _banner != null | ||||
|                               ? AutoResizeUniversalImage( | ||||
|                                   sn.getAttachmentUrl(_banner!), | ||||
|                                   fit: BoxFit.cover, | ||||
|                                 ) | ||||
|                               : const SizedBox.shrink(), | ||||
|                           child: | ||||
|                               _banner != null | ||||
|                                   ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) | ||||
|                                   : const SizedBox.shrink(), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
| @@ -242,9 +250,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|             const Gap(4), | ||||
|             TextField( | ||||
|               controller: _nickController, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'fieldNickname'.tr(), | ||||
|               ), | ||||
|               decoration: InputDecoration(labelText: 'fieldNickname'.tr()), | ||||
|               onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ), | ||||
|             const Gap(4), | ||||
| @@ -252,9 +258,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|               controller: _descriptionController, | ||||
|               maxLines: null, | ||||
|               minLines: 3, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'fieldDescription'.tr(), | ||||
|               ), | ||||
|               decoration: InputDecoration(labelText: 'fieldDescription'.tr()), | ||||
|               onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ), | ||||
|             const Gap(12), | ||||
| @@ -275,7 +279,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|                   icon: const Icon(Symbols.save), | ||||
|                 ), | ||||
|               ], | ||||
|             ) | ||||
|             ), | ||||
|           ], | ||||
|         ).padding(horizontal: 24, vertical: 12), | ||||
|       ), | ||||
|   | ||||
| @@ -109,7 +109,7 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> { | ||||
|  | ||||
|     _nameController.text = ua.user!.name; | ||||
|     _nickController.text = ua.user!.nick; | ||||
|     _descriptionController.text = ua.user!.description; | ||||
|     _descriptionController.text = ua.user!.profile!.description; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -74,18 +74,20 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       final ud = context.read<UserDirectoryProvider>(); | ||||
|       final idSet = <int>{}; | ||||
|       for (final channel in channels) { | ||||
|         if (channel.type == 1) { | ||||
|           await ud.listAccount( | ||||
|           idSet.addAll( | ||||
|             channel.members | ||||
|                     ?.cast<SnChannelMember?>() | ||||
|                     .map((ele) => ele?.accountId) | ||||
|                     .where((ele) => ele != null) | ||||
|                     .toSet() ?? | ||||
|                 {}, | ||||
|                     .cast<int>() ?? | ||||
|                 [], | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|       if (idSet.isNotEmpty) await ud.listAccount(idSet); | ||||
|  | ||||
|       if (mounted) setState(() => _channels = channels); | ||||
|     }) | ||||
| @@ -161,7 +163,6 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ud = context.read<UserDirectoryProvider>(); | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
| @@ -203,7 +204,6 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|               Theme.of(context).floatingActionButtonTheme.foregroundColor, | ||||
|           backgroundColor: | ||||
|               Theme.of(context).floatingActionButtonTheme.backgroundColor, | ||||
|           shape: const CircleBorder(), | ||||
|         ), | ||||
|         closeButtonBuilder: DefaultFloatingActionButtonBuilder( | ||||
|           child: const Icon(Symbols.close, size: 28), | ||||
| @@ -212,7 +212,6 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|               Theme.of(context).floatingActionButtonTheme.foregroundColor, | ||||
|           backgroundColor: | ||||
|               Theme.of(context).floatingActionButtonTheme.backgroundColor, | ||||
|           shape: const CircleBorder(), | ||||
|         ), | ||||
|         children: [ | ||||
|           Row( | ||||
| @@ -264,117 +263,10 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|                     final channel = _channels![idx]; | ||||
|                     final lastMessage = _lastMessages?[channel.id]; | ||||
|  | ||||
|                     if (channel.type == 1) { | ||||
|                       final otherMember = | ||||
|                           channel.members?.cast<SnChannelMember?>().firstWhere( | ||||
|                                 (ele) => ele?.accountId != ua.user?.id, | ||||
|                                 orElse: () => null, | ||||
|                               ); | ||||
|  | ||||
|                       return ListTile( | ||||
|                         title: Row( | ||||
|                           children: [ | ||||
|                             Expanded( | ||||
|                               child: Text(ud | ||||
|                                       .getAccountFromCache( | ||||
|                                           otherMember?.accountId) | ||||
|                                       ?.nick ?? | ||||
|                                   channel.name), | ||||
|                             ), | ||||
|                             const Gap(8), | ||||
|                             if (_unreadCounts?[channel.id] != null && | ||||
|                                 _unreadCounts![channel.id]! > 0) | ||||
|                               Badge( | ||||
|                                 label: Text('${_unreadCounts![channel.id]}'), | ||||
|                               ), | ||||
|                           ], | ||||
|                         ), | ||||
|                         subtitle: lastMessage != null | ||||
|                             ? Text( | ||||
|                                 '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                                 maxLines: 1, | ||||
|                                 overflow: TextOverflow.ellipsis, | ||||
|                               ) | ||||
|                             : Text( | ||||
|                                 channel.description, | ||||
|                                 maxLines: 1, | ||||
|                                 overflow: TextOverflow.ellipsis, | ||||
|                               ), | ||||
|                         contentPadding: | ||||
|                             const EdgeInsets.symmetric(horizontal: 16), | ||||
|                         leading: AccountImage( | ||||
|                           content: ud | ||||
|                               .getAccountFromCache(otherMember?.accountId) | ||||
|                               ?.avatar, | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           _onTapChannel(channel); | ||||
|                         }, | ||||
|                       ); | ||||
|                     } | ||||
|  | ||||
|                     return ListTile( | ||||
|                       title: Row( | ||||
|                         children: [ | ||||
|                           Expanded(child: Text(channel.name)), | ||||
|                           const Gap(8), | ||||
|                           if (_unreadCounts?[channel.id] != null && | ||||
|                               _unreadCounts![channel.id]! > 0) | ||||
|                             Badge( | ||||
|                               label: Text('${_unreadCounts![channel.id]}'), | ||||
|                             ), | ||||
|                         ], | ||||
|                       ), | ||||
|                       subtitle: lastMessage != null | ||||
|                           ? Row( | ||||
|                               children: [ | ||||
|                                 Badge( | ||||
|                                   label: Text(ud | ||||
|                                           .getAccountFromCache( | ||||
|                                               lastMessage.sender.accountId) | ||||
|                                           ?.nick ?? | ||||
|                                       'unknown'.tr()), | ||||
|                                   backgroundColor: | ||||
|                                       Theme.of(context).colorScheme.primary, | ||||
|                                 ), | ||||
|                                 const Gap(6), | ||||
|                                 Expanded( | ||||
|                                   child: Text( | ||||
|                                     lastMessage.body['text'] ?? | ||||
|                                         'Unable preview', | ||||
|                                     maxLines: 1, | ||||
|                                     overflow: TextOverflow.ellipsis, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 Text( | ||||
|                                   DateFormat( | ||||
|                                     lastMessage.createdAt.toLocal().day == | ||||
|                                             DateTime.now().day | ||||
|                                         ? 'HH:mm' | ||||
|                                         : lastMessage.createdAt | ||||
|                                                     .toLocal() | ||||
|                                                     .year == | ||||
|                                                 DateTime.now().year | ||||
|                                             ? 'MM/dd' | ||||
|                                             : 'yy/MM/dd', | ||||
|                                   ).format(lastMessage.createdAt.toLocal()), | ||||
|                                   style: GoogleFonts.robotoMono( | ||||
|                                     fontSize: 12, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ) | ||||
|                           : Text( | ||||
|                               channel.description, | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ), | ||||
|                       contentPadding: | ||||
|                           const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       leading: AccountImage( | ||||
|                         content: channel.realm?.avatar, | ||||
|                         fallbackWidget: const Icon(Symbols.chat, size: 20), | ||||
|                       ), | ||||
|                     return _ChatChannelEntry( | ||||
|                       channel: channel, | ||||
|                       lastMessage: lastMessage, | ||||
|                       unreadCount: _unreadCounts?[channel.id], | ||||
|                       onTap: () { | ||||
|                         if (doExpand) { | ||||
|                           _unreadCounts?[channel.id] = 0; | ||||
| @@ -416,3 +308,100 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|     return chatList; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ChatChannelEntry extends StatelessWidget { | ||||
|   final SnChannel channel; | ||||
|   final int? unreadCount; | ||||
|   final SnChatMessage? lastMessage; | ||||
|   final Function? onTap; | ||||
|   const _ChatChannelEntry({ | ||||
|     required this.channel, | ||||
|     this.unreadCount, | ||||
|     this.lastMessage, | ||||
|     this.onTap, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ud = context.read<UserDirectoryProvider>(); | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     final otherMember = channel.type == 1 | ||||
|         ? channel.members?.cast<SnChannelMember?>().firstWhere( | ||||
|               (ele) => ele?.accountId != ua.user?.id, | ||||
|               orElse: () => null, | ||||
|             ) | ||||
|         : null; | ||||
|  | ||||
|     final title = otherMember != null | ||||
|         ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name | ||||
|         : channel.name; | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Row( | ||||
|         children: [ | ||||
|           Expanded(child: Text(title)), | ||||
|           const Gap(8), | ||||
|           if (unreadCount != null && unreadCount! > 0) | ||||
|             Badge( | ||||
|               label: Text(unreadCount.toString()), | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|       subtitle: lastMessage != null | ||||
|           ? Row( | ||||
|               children: [ | ||||
|                 Badge( | ||||
|                   label: Text( | ||||
|                       ud.getFromCache(lastMessage!.sender.accountId)?.nick ?? | ||||
|                           'unknown'.tr()), | ||||
|                   backgroundColor: Theme.of(context).colorScheme.primary, | ||||
|                   textColor: Theme.of(context).colorScheme.onPrimary, | ||||
|                 ), | ||||
|                 const Gap(6), | ||||
|                 Expanded( | ||||
|                   child: Text( | ||||
|                     lastMessage!.body['algorithm'] == 'plain' | ||||
|                         ? lastMessage!.body['text'] ?? | ||||
|                             'messageUnablePreview'.tr() | ||||
|                         : 'messageUnablePreviewEncrypted'.tr(), | ||||
|                     maxLines: 1, | ||||
|                     overflow: TextOverflow.ellipsis, | ||||
|                     style: lastMessage!.body['algorithm'] != 'plain' || | ||||
|                             lastMessage!.body['text'] == null | ||||
|                         ? TextStyle(fontStyle: FontStyle.italic) | ||||
|                         : null, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 Text( | ||||
|                   DateFormat( | ||||
|                     lastMessage!.createdAt.toLocal().day == DateTime.now().day | ||||
|                         ? 'HH:mm' | ||||
|                         : lastMessage!.createdAt.toLocal().year == | ||||
|                                 DateTime.now().year | ||||
|                             ? 'MM/dd' | ||||
|                             : 'yy/MM/dd', | ||||
|                   ).format(lastMessage!.createdAt.toLocal()), | ||||
|                   style: GoogleFonts.robotoMono( | ||||
|                     fontSize: 12, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ) | ||||
|           : Text( | ||||
|               channel.description, | ||||
|               maxLines: 1, | ||||
|               overflow: TextOverflow.ellipsis, | ||||
|             ), | ||||
|       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|       leading: AccountImage( | ||||
|         content: otherMember != null | ||||
|             ? ud.getFromCache(otherMember.accountId)?.avatar | ||||
|             : channel.realm?.avatar, | ||||
|         fallbackWidget: const Icon(Symbols.chat, size: 20), | ||||
|       ), | ||||
|       onTap: () => onTap?.call(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -57,11 +57,10 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = | ||||
|           await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/me'); | ||||
|       _profile = SnChannelMember.fromJson(resp.data); | ||||
|       _notifyLevel = _profile!.notify; | ||||
|       final ct = context.read<ChatChannelProvider>(); | ||||
|       final resp = await ct.getChannelProfile(_channel!); | ||||
|       _profile = resp; | ||||
|       _notifyLevel = resp.notify; | ||||
|       if (!mounted) return; | ||||
|       final ud = context.read<UserDirectoryProvider>(); | ||||
|       await ud.getAccount(_profile!.accountId); | ||||
| @@ -103,10 +102,12 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     try { | ||||
|       final ct = context.read<ChatChannelProvider>(); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.delete( | ||||
|         '/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me', | ||||
|       ); | ||||
|       await ct.removeLocalChannel(_channel!); | ||||
|       if (!mounted) return; | ||||
|       Navigator.pop(context, false); | ||||
|     } catch (err) { | ||||
| @@ -130,12 +131,15 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|     setState(() => _isUpdatingNotifyLevel = true); | ||||
|  | ||||
|     try { | ||||
|       final ct = context.read<ChatChannelProvider>(); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.put( | ||||
|       final resp = await sn.client.put( | ||||
|         '/cgi/im/channels/${_channel!.keyPath}/members/me/notify', | ||||
|         data: {'notify_level': value}, | ||||
|       ); | ||||
|       _profile = SnChannelMember.fromJson(resp.data); | ||||
|       _notifyLevel = value; | ||||
|       await ct.updateChannelProfile(_profile!); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('channelNotifyLevelApplied'.tr()); | ||||
|     } catch (err) { | ||||
| @@ -289,15 +293,14 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|                   ), | ||||
|                   ListTile( | ||||
|                     leading: AccountImage( | ||||
|                       content: | ||||
|                           ud.getAccountFromCache(_profile!.accountId)?.avatar, | ||||
|                       content: ud.getFromCache(_profile!.accountId)?.avatar, | ||||
|                       radius: 18, | ||||
|                     ), | ||||
|                     trailing: const Icon(Symbols.chevron_right), | ||||
|                     title: Text('channelEditProfile').tr(), | ||||
|                     subtitle: Text( | ||||
|                       (_profile?.nick?.isEmpty ?? true) | ||||
|                           ? ud.getAccountFromCache(_profile!.accountId)!.nick | ||||
|                           ? ud.getFromCache(_profile!.accountId)!.nick | ||||
|                           : _profile!.nick!, | ||||
|                     ), | ||||
|                     contentPadding: const EdgeInsets.only(left: 20, right: 20), | ||||
| @@ -408,11 +411,14 @@ class _ChannelProfileDetailDialogState | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final ct = context.read<ChatChannelProvider>(); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.put( | ||||
|       final resp = await sn.client.put( | ||||
|         '/cgi/im/channels/${widget.channel.keyPath}/members/me', | ||||
|         data: {'nick': _nickController.text}, | ||||
|       ); | ||||
|       final out = SnChannelMember.fromJson(resp.data); | ||||
|       await ct.updateChannelProfile(out); | ||||
|       if (!mounted) return; | ||||
|       Navigator.pop(context, true); | ||||
|     } catch (err) { | ||||
| @@ -575,11 +581,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
|                 return ListTile( | ||||
|                   contentPadding: const EdgeInsets.only(right: 24, left: 16), | ||||
|                   leading: AccountImage( | ||||
|                     content: ud.getAccountFromCache(member.accountId)?.avatar, | ||||
|                     content: ud.getFromCache(member.accountId)?.avatar, | ||||
|                   ), | ||||
|                   title: Text( | ||||
|                     ud.getAccountFromCache(member.accountId)?.name ?? | ||||
|                         'unknown'.tr(), | ||||
|                     ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(), | ||||
|                   ), | ||||
|                   subtitle: Text(member.nick ?? 'unknown'.tr()), | ||||
|                   trailing: SizedBox( | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| @@ -19,6 +20,7 @@ import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/types/websocket.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_prejoin.dart'; | ||||
| import 'package:surface/widgets/chat/chat_message.dart'; | ||||
| import 'package:surface/widgets/chat/chat_message_input.dart'; | ||||
| @@ -58,6 +60,11 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|   final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey(); | ||||
|   late final ChatMessageController _messageController; | ||||
|  | ||||
|   late final NotificationProvider _nty = context.read<NotificationProvider>(); | ||||
|   late final WebSocketProvider _ws = context.read<WebSocketProvider>(); | ||||
|  | ||||
|   bool _isEncrypted = false; | ||||
|  | ||||
|   StreamSubscription? _wsSubscription; | ||||
|  | ||||
|   // TODO fetch user identity and ask them to join the channel or not | ||||
| @@ -87,8 +94,18 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|       } | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       final nty = context.read<NotificationProvider>(); | ||||
|       nty.skippableNotifyChannel = _channel!.id; | ||||
|       _nty.skippableNotifyChannel = _channel!.id; | ||||
|       final ws = context.read<WebSocketProvider>(); | ||||
|       if (_channel != null) { | ||||
|         ws.conn?.sink.add( | ||||
|           jsonEncode(WebSocketPackage( | ||||
|               method: 'events.subscribe', | ||||
|               endpoint: 'im', | ||||
|               payload: { | ||||
|                 'channel_id': _channel!.id, | ||||
|               })), | ||||
|         ); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
| @@ -214,8 +231,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|       ]); | ||||
|     }); | ||||
|  | ||||
|     final ws = context.read<WebSocketProvider>(); | ||||
|     _wsSubscription = ws.pk.stream.listen((event) { | ||||
|     _wsSubscription = _ws.pk.stream.listen((event) { | ||||
|       switch (event.method) { | ||||
|         case 'calls.new': | ||||
|           final payload = SnChatCall.fromJson(event.payload!); | ||||
| @@ -237,8 +253,18 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|   void dispose() { | ||||
|     _wsSubscription?.cancel(); | ||||
|     _messageController.dispose(); | ||||
|     final nty = context.read<NotificationProvider>(); | ||||
|     nty.skippableNotifyChannel = null; | ||||
|     _nty.skippableNotifyChannel = null; | ||||
|     if (_channel != null) { | ||||
|       _ws.conn?.sink.add( | ||||
|         jsonEncode(WebSocketPackage( | ||||
|           method: 'events.unsubscribe', | ||||
|           endpoint: 'im', | ||||
|           payload: { | ||||
|             'channel_id': _channel!.id, | ||||
|           }, | ||||
|         )), | ||||
|       ); | ||||
|     } | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| @@ -251,11 +277,19 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|       appBar: AppBar( | ||||
|         title: Text( | ||||
|           _channel?.type == 1 | ||||
|               ? ud.getAccountFromCache(_otherMember?.accountId)?.nick ?? | ||||
|                   _channel!.name | ||||
|               ? ud.getFromCache(_otherMember?.accountId)?.nick ?? _channel!.name | ||||
|               : _channel?.name ?? 'loading'.tr(), | ||||
|         ), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               setState(() => _isEncrypted = !_isEncrypted); | ||||
|               _inputGlobalKey.currentState?.setEncrypted(_isEncrypted); | ||||
|             }, | ||||
|             icon: _isEncrypted | ||||
|                 ? const Icon(Symbols.lock) | ||||
|                 : const Icon(Symbols.no_encryption), | ||||
|           ), | ||||
|           IconButton( | ||||
|             icon: _ongoingCall == null | ||||
|                 ? const Icon(Symbols.call) | ||||
| @@ -289,7 +323,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|         builder: (context, _) { | ||||
|           return Column( | ||||
|             children: [ | ||||
|               LoadingIndicator(isActive: _isBusy), | ||||
|               LoadingIndicator( | ||||
|                 isActive: _isBusy || _messageController.isAggressiveLoading, | ||||
|               ), | ||||
|               SingleChildScrollView( | ||||
|                 physics: const NeverScrollableScrollPhysics(), | ||||
|                 child: MaterialBanner( | ||||
| @@ -315,8 +351,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|               if (_messageController.isPending) | ||||
|                 Expanded( | ||||
|                   child: const CircularProgressIndicator().center(), | ||||
|                 ), | ||||
|               if (!_messageController.isPending) | ||||
|                 ) | ||||
|               else | ||||
|                 Expanded( | ||||
|                   child: InfiniteList( | ||||
|                     reverse: true, | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; | ||||
| @@ -19,6 +18,9 @@ import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| const kPostChannels = ['Global', 'Friends', 'Following']; | ||||
| const kPostChannelIcons = [Symbols.globe, Symbols.group, Symbols.subscriptions]; | ||||
|  | ||||
| const Map<String, IconData> kCategoryIcons = { | ||||
|   'technology': Symbols.tools_wrench, | ||||
|   'gaming': Symbols.gamepad, | ||||
| @@ -39,17 +41,17 @@ class ExploreScreen extends StatefulWidget { | ||||
|   State<ExploreScreen> createState() => _ExploreScreenState(); | ||||
| } | ||||
|  | ||||
| // You know what? I'm not going to make this a global variable. | ||||
| // Cuz the global key make the selected category not update to child widget when the category is changed. | ||||
| SnPostCategory? _selectedCategory; | ||||
|  | ||||
| class _ExploreScreenState extends State<ExploreScreen> | ||||
|     with SingleTickerProviderStateMixin { | ||||
|   late final TabController _tabController = | ||||
|       TabController(length: 4, vsync: this); | ||||
|     with TickerProviderStateMixin { | ||||
|   late TabController _tabController = TabController( | ||||
|     length: kPostChannels.length, | ||||
|     vsync: this, | ||||
|   ); | ||||
|  | ||||
|   final _fabKey = GlobalKey<ExpandableFabState>(); | ||||
|   final _listKeys = List.generate(4, (_) => GlobalKey<_PostListWidgetState>()); | ||||
|   final _listKey = GlobalKey<_PostListWidgetState>(); | ||||
|  | ||||
|   bool _showCategories = false; | ||||
|  | ||||
|   final List<SnPostCategory> _categories = List.empty(growable: true); | ||||
|  | ||||
| @@ -69,14 +71,64 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _clearFilter() { | ||||
|     _selectedCategory = null; | ||||
|   final List<SnRealm> _realms = List.empty(growable: true); | ||||
|  | ||||
|   Future<void> _fetchRealms() async { | ||||
|     try { | ||||
|       final rels = context.read<SnRealmProvider>(); | ||||
|       final out = await rels.listAvailableRealms(); | ||||
|       setState(() { | ||||
|         _realms.addAll(out); | ||||
|       }); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _toggleShowCategories() { | ||||
|     _showCategories = !_showCategories; | ||||
|     if (_showCategories) { | ||||
|       _tabController = TabController(length: _categories.length, vsync: this); | ||||
|     } else { | ||||
|       _tabController = TabController(length: kPostChannels.length, vsync: this); | ||||
|     } | ||||
|     _tabListen(); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void _tabListen() { | ||||
|     _tabController.addListener(() { | ||||
|       if (_tabController.indexIsChanging) { | ||||
|         if (_showCategories) { | ||||
|           _listKey.currentState?.setCategory(_categories[_tabController.index]); | ||||
|           _listKey.currentState?.refreshPosts(); | ||||
|           return; | ||||
|         } | ||||
|         switch (_tabController.index) { | ||||
|           case 0: | ||||
|           case 3: | ||||
|             _listKey.currentState?.setChannel(null); | ||||
|             break; | ||||
|           case 1: | ||||
|             _listKey.currentState?.setChannel('friends'); | ||||
|             break; | ||||
|           case 2: | ||||
|             _listKey.currentState?.setChannel('following'); | ||||
|             break; | ||||
|         } | ||||
|         _listKey.currentState?.refreshPosts(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     _fetchCategories(); | ||||
|     super.initState(); | ||||
|     _tabListen(); | ||||
|     _fetchCategories(); | ||||
|     _fetchRealms(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -86,7 +138,7 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|   } | ||||
|  | ||||
|   Future<void> refreshPosts() async { | ||||
|     await _listKeys[_tabController.index].currentState?.refreshPosts(); | ||||
|     await _listKey.currentState?.refreshPosts(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -111,7 +163,6 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|               Theme.of(context).floatingActionButtonTheme.foregroundColor, | ||||
|           backgroundColor: | ||||
|               Theme.of(context).floatingActionButtonTheme.backgroundColor, | ||||
|           shape: const CircleBorder(), | ||||
|         ), | ||||
|         closeButtonBuilder: DefaultFloatingActionButtonBuilder( | ||||
|           child: const Icon(Symbols.close, size: 28), | ||||
| @@ -120,90 +171,24 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|               Theme.of(context).floatingActionButtonTheme.foregroundColor, | ||||
|           backgroundColor: | ||||
|               Theme.of(context).floatingActionButtonTheme.backgroundColor, | ||||
|           shape: const CircleBorder(), | ||||
|         ), | ||||
|         children: [ | ||||
|           Row( | ||||
|             children: [ | ||||
|               Text('writePostTypeStory').tr(), | ||||
|               Text('writePost').tr(), | ||||
|               const Gap(20), | ||||
|               FloatingActionButton( | ||||
|                 heroTag: null, | ||||
|                 tooltip: 'writePostTypeStory'.tr(), | ||||
|                 tooltip: 'writePost'.tr(), | ||||
|                 onPressed: () { | ||||
|                   GoRouter.of(context).pushNamed('postEditor', pathParameters: { | ||||
|                     'mode': 'stories', | ||||
|                   }).then((value) { | ||||
|                   GoRouter.of(context).pushNamed('postEditor').then((value) { | ||||
|                     if (value == true) { | ||||
|                       refreshPosts(); | ||||
|                     } | ||||
|                   }); | ||||
|                   _fabKey.currentState!.toggle(); | ||||
|                 }, | ||||
|                 child: const Icon(Symbols.post_rounded), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           Row( | ||||
|             children: [ | ||||
|               Text('writePostTypeArticle').tr(), | ||||
|               const Gap(20), | ||||
|               FloatingActionButton( | ||||
|                 heroTag: null, | ||||
|                 tooltip: 'writePostTypeArticle'.tr(), | ||||
|                 onPressed: () { | ||||
|                   GoRouter.of(context).pushNamed('postEditor', pathParameters: { | ||||
|                     'mode': 'articles', | ||||
|                   }).then((value) { | ||||
|                     if (value == true) { | ||||
|                       refreshPosts(); | ||||
|                     } | ||||
|                   }); | ||||
|                   _fabKey.currentState!.toggle(); | ||||
|                 }, | ||||
|                 child: const Icon(Symbols.news), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           Row( | ||||
|             children: [ | ||||
|               Text('writePostTypeQuestion').tr(), | ||||
|               const Gap(20), | ||||
|               FloatingActionButton( | ||||
|                 heroTag: null, | ||||
|                 tooltip: 'writePostTypeQuestion'.tr(), | ||||
|                 onPressed: () { | ||||
|                   GoRouter.of(context).pushNamed('postEditor', pathParameters: { | ||||
|                     'mode': 'questions', | ||||
|                   }).then((value) { | ||||
|                     if (value == true) { | ||||
|                       refreshPosts(); | ||||
|                     } | ||||
|                   }); | ||||
|                   _fabKey.currentState!.toggle(); | ||||
|                 }, | ||||
|                 child: const Icon(Symbols.question_answer), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           Row( | ||||
|             children: [ | ||||
|               Text('writePostTypeVideo').tr(), | ||||
|               const Gap(20), | ||||
|               FloatingActionButton( | ||||
|                 heroTag: null, | ||||
|                 tooltip: 'writePostTypeVideo'.tr(), | ||||
|                 onPressed: () { | ||||
|                   GoRouter.of(context).pushNamed('postEditor', pathParameters: { | ||||
|                     'mode': 'videos', | ||||
|                   }).then((value) { | ||||
|                     if (value == true) { | ||||
|                       refreshPosts(); | ||||
|                     } | ||||
|                   }); | ||||
|                   _fabKey.currentState!.toggle(); | ||||
|                 }, | ||||
|                 child: const Icon(Symbols.video_call), | ||||
|                 child: const Icon(Symbols.edit), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
| @@ -216,25 +201,62 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|               handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||
|               sliver: SliverAppBar( | ||||
|                 leading: AutoAppBarLeading(), | ||||
|                 title: Text('screenExplore').tr(), | ||||
|                 titleSpacing: 0, | ||||
|                 title: Row( | ||||
|                   children: [ | ||||
|                     IconButton( | ||||
|                       padding: EdgeInsets.zero, | ||||
|                       constraints: const BoxConstraints(), | ||||
|                       visualDensity: VisualDensity.compact, | ||||
|                       icon: _listKey.currentState?.realm != null | ||||
|                           ? AccountImage( | ||||
|                               content: _listKey.currentState!.realm!.avatar, | ||||
|                               radius: 14, | ||||
|                             ) | ||||
|                           : const Icon(Symbols.group), | ||||
|                       onPressed: () { | ||||
|                         showModalBottomSheet( | ||||
|                           context: context, | ||||
|                           builder: (context) => _PostListRealmPopup( | ||||
|                             realms: _realms, | ||||
|                             onUpdate: (realm) { | ||||
|                               _listKey.currentState?.setRealm(realm); | ||||
|                               _listKey.currentState?.refreshPosts(); | ||||
|                               Future.delayed(const Duration(milliseconds: 100), | ||||
|                                   () { | ||||
|                                 if (mounted) { | ||||
|                                   setState(() {}); | ||||
|                                 } | ||||
|                               }); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                     Expanded( | ||||
|                       child: Center( | ||||
|                         child: Text('screenExplore').tr(), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 floating: true, | ||||
|                 snap: true, | ||||
|                 actions: [ | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Symbols.category), | ||||
|                     style: _showCategories | ||||
|                         ? ButtonStyle( | ||||
|                             foregroundColor: WidgetStateProperty.all( | ||||
|                               Theme.of(context).colorScheme.primary, | ||||
|                             ), | ||||
|                             backgroundColor: MaterialStateProperty.all( | ||||
|                               Theme.of(context).colorScheme.secondaryContainer, | ||||
|                             ), | ||||
|                           ) | ||||
|                         : null, | ||||
|                     onPressed: () { | ||||
|                       showModalBottomSheet( | ||||
|                         context: context, | ||||
|                         builder: (context) => _PostCategoryPickerPopup( | ||||
|                           categories: _categories, | ||||
|                           selected: _selectedCategory, | ||||
|                         ), | ||||
|                       ).then((value) { | ||||
|                         if (value != null && context.mounted) { | ||||
|                           _selectedCategory = value == false ? null : value; | ||||
|                           refreshPosts(); | ||||
|                         } | ||||
|                       }); | ||||
|                       _toggleShowCategories(); | ||||
|                     }, | ||||
|                   ), | ||||
|                   IconButton( | ||||
| @@ -246,122 +268,74 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|                   const Gap(8), | ||||
|                 ], | ||||
|                 bottom: TabBar( | ||||
|                   isScrollable: _showCategories, | ||||
|                   controller: _tabController, | ||||
|                   tabs: [ | ||||
|                     Tab( | ||||
|                       child: Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Icon(Symbols.globe, | ||||
|                               size: 20, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor), | ||||
|                           const Gap(8), | ||||
|                           Flexible( | ||||
|                             child: Text( | ||||
|                               'postChannelGlobal', | ||||
|                               maxLines: 1, | ||||
|                             ).tr().textColor( | ||||
|                                 Theme.of(context).appBarTheme.foregroundColor), | ||||
|                           ), | ||||
|                   tabs: _showCategories | ||||
|                       ? [ | ||||
|                           for (final category in _categories) | ||||
|                             Tab( | ||||
|                               child: Row( | ||||
|                                 mainAxisSize: MainAxisSize.min, | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                                 children: [ | ||||
|                                   Icon( | ||||
|                                       kCategoryIcons[category.alias] ?? | ||||
|                                           Symbols.question_mark, | ||||
|                                       color: Theme.of(context) | ||||
|                                           .appBarTheme | ||||
|                                           .foregroundColor!), | ||||
|                                   const Gap(8), | ||||
|                                   Flexible( | ||||
|                                     child: Text( | ||||
|                                       'postCategory${category.alias.capitalize()}' | ||||
|                                               .trExists() | ||||
|                                           ? 'postCategory${category.alias.capitalize()}' | ||||
|                                               .tr() | ||||
|                                           : category.name, | ||||
|                                       maxLines: 1, | ||||
|                                     ).textColor(Theme.of(context) | ||||
|                                         .appBarTheme | ||||
|                                         .foregroundColor!), | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                         ] | ||||
|                       : [ | ||||
|                           for (final channel in kPostChannels) | ||||
|                             Tab( | ||||
|                               child: Row( | ||||
|                                 mainAxisSize: MainAxisSize.min, | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                                 children: [ | ||||
|                                   Icon( | ||||
|                                     kPostChannelIcons[ | ||||
|                                         kPostChannels.indexOf(channel)], | ||||
|                                     size: 20, | ||||
|                                     color: Theme.of(context) | ||||
|                                         .appBarTheme | ||||
|                                         .foregroundColor, | ||||
|                                   ), | ||||
|                                   const Gap(8), | ||||
|                                   Flexible( | ||||
|                                     child: Text( | ||||
|                                       'postChannel$channel', | ||||
|                                       maxLines: 1, | ||||
|                                     ).tr().textColor(Theme.of(context) | ||||
|                                         .appBarTheme | ||||
|                                         .foregroundColor), | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                     Tab( | ||||
|                       child: Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Icon(Symbols.group, | ||||
|                               size: 20, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor), | ||||
|                           const Gap(8), | ||||
|                           Flexible( | ||||
|                             child: Text( | ||||
|                               'postChannelFriends', | ||||
|                               maxLines: 1, | ||||
|                               textAlign: TextAlign.center, | ||||
|                             ).tr().textColor( | ||||
|                                 Theme.of(context).appBarTheme.foregroundColor), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                     Tab( | ||||
|                       child: Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Icon(Symbols.subscriptions, | ||||
|                               size: 20, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor), | ||||
|                           const Gap(8), | ||||
|                           Flexible( | ||||
|                             child: Text( | ||||
|                               'postChannelFollowing', | ||||
|                               maxLines: 1, | ||||
|                             ).tr().textColor( | ||||
|                                 Theme.of(context).appBarTheme.foregroundColor), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                     Tab( | ||||
|                       child: Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Icon(Symbols.workspaces, | ||||
|                               size: 20, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor), | ||||
|                           const Gap(8), | ||||
|                           Flexible( | ||||
|                             child: Text( | ||||
|                               'postChannelRealm', | ||||
|                               maxLines: 1, | ||||
|                             ).tr().textColor( | ||||
|                                 Theme.of(context).appBarTheme.foregroundColor), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ]; | ||||
|         }, | ||||
|         body: TabBarView( | ||||
|           controller: _tabController, | ||||
|           children: [ | ||||
|             _PostListWidget( | ||||
|               key: _listKeys[0], | ||||
|               onClearFilter: _clearFilter, | ||||
|             ), | ||||
|             _PostListWidget( | ||||
|               key: _listKeys[1], | ||||
|               channel: 'friends', | ||||
|               onClearFilter: _clearFilter, | ||||
|             ), | ||||
|             _PostListWidget( | ||||
|               key: _listKeys[2], | ||||
|               channel: 'following', | ||||
|               onClearFilter: _clearFilter, | ||||
|             ), | ||||
|             _PostListWidget( | ||||
|               key: _listKeys[3], | ||||
|               withRealm: true, | ||||
|               onClearFilter: _clearFilter, | ||||
|             ), | ||||
|           ], | ||||
|         body: _PostListWidget( | ||||
|           key: _listKey, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
| @@ -369,15 +343,7 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
| } | ||||
|  | ||||
| class _PostListWidget extends StatefulWidget { | ||||
|   final String? channel; | ||||
|   final bool withRealm; | ||||
|   final Function onClearFilter; | ||||
|  | ||||
|   const _PostListWidget( | ||||
|       {super.key, | ||||
|       this.channel, | ||||
|       this.withRealm = false, | ||||
|       required this.onClearFilter}); | ||||
|   const _PostListWidget({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<_PostListWidget> createState() => _PostListWidgetState(); | ||||
| @@ -386,25 +352,13 @@ class _PostListWidget extends StatefulWidget { | ||||
| class _PostListWidgetState extends State<_PostListWidget> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   final List<SnPost> _posts = List.empty(growable: true); | ||||
|   final List<SnRealm> _realms = List.empty(growable: true); | ||||
|   SnRealm? _selectedRealm; | ||||
|   int? _postCount; | ||||
|   SnRealm? get realm => _selectedRealm; | ||||
|  | ||||
|   Future<void> _fetchRealms() async { | ||||
|     try { | ||||
|       final rels = context.read<SnRealmProvider>(); | ||||
|       final out = await rels.listAvailableRealms(); | ||||
|       setState(() { | ||||
|         _realms.addAll(out); | ||||
|         _selectedRealm = out.firstOrNull; | ||||
|       }); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|   final List<SnPost> _posts = List.empty(growable: true); | ||||
|   SnRealm? _selectedRealm; | ||||
|   String? _selectedChannel; | ||||
|   SnPostCategory? _selectedCategory; | ||||
|   int? _postCount; | ||||
|  | ||||
|   Future<void> _fetchPosts() async { | ||||
|     if (_postCount != null && _posts.length >= _postCount!) return; | ||||
| @@ -416,7 +370,7 @@ class _PostListWidgetState extends State<_PostListWidget> { | ||||
|       take: 10, | ||||
|       offset: _posts.length, | ||||
|       categories: _selectedCategory != null ? [_selectedCategory!.alias] : null, | ||||
|       channel: widget.channel, | ||||
|       channel: _selectedChannel, | ||||
|       realm: _selectedRealm?.alias, | ||||
|     ); | ||||
|     final out = result.$1; | ||||
| @@ -429,6 +383,21 @@ class _PostListWidgetState extends State<_PostListWidget> { | ||||
|     if (mounted) setState(() => _isBusy = false); | ||||
|   } | ||||
|  | ||||
|   void setChannel(String? channel) { | ||||
|     _selectedChannel = channel; | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void setRealm(SnRealm? realm) { | ||||
|     _selectedRealm = realm; | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void setCategory(SnPostCategory? category) { | ||||
|     _selectedCategory = category; | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   Future<void> refreshPosts() { | ||||
|     _postCount = null; | ||||
|     _posts.clear(); | ||||
| @@ -438,13 +407,7 @@ class _PostListWidgetState extends State<_PostListWidget> { | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     if (widget.withRealm) { | ||||
|       _fetchRealms().then((_) { | ||||
|         _fetchPosts(); | ||||
|       }); | ||||
|     } else { | ||||
|       _fetchPosts(); | ||||
|     } | ||||
|     _fetchPosts(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -467,52 +430,13 @@ class _PostListWidgetState extends State<_PostListWidget> { | ||||
|               IconButton( | ||||
|                 icon: const Icon(Symbols.clear), | ||||
|                 onPressed: () { | ||||
|                   widget.onClearFilter.call(); | ||||
|                   setState(() => _selectedCategory = null); | ||||
|                   refreshPosts(); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|             padding: const EdgeInsets.only(left: 20, right: 4), | ||||
|           ), | ||||
|         if (widget.withRealm) | ||||
|           DropdownButtonHideUnderline( | ||||
|             child: DropdownButton2<SnRealm>( | ||||
|               isExpanded: true, | ||||
|               items: _realms | ||||
|                   .map( | ||||
|                     (ele) => DropdownMenuItem<SnRealm>( | ||||
|                       value: ele, | ||||
|                       child: Row( | ||||
|                         children: [ | ||||
|                           AccountImage( | ||||
|                             content: ele.avatar, | ||||
|                             fallbackWidget: const Icon(Symbols.group, size: 16), | ||||
|                             radius: 14, | ||||
|                           ), | ||||
|                           const Gap(8), | ||||
|                           Text( | ||||
|                             ele.name, | ||||
|                             style: Theme.of(context).textTheme.bodyMedium, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ) | ||||
|                   .toList(), | ||||
|               value: _selectedRealm, | ||||
|               onChanged: (SnRealm? value) { | ||||
|                 setState(() => _selectedRealm = value); | ||||
|                 refreshPosts(); | ||||
|               }, | ||||
|               buttonStyleData: const ButtonStyleData( | ||||
|                 padding: EdgeInsets.only(left: 4, right: 12), | ||||
|               ), | ||||
|               menuItemStyleData: const MenuItemStyleData( | ||||
|                 height: 48, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         if (widget.withRealm) const Divider(height: 1), | ||||
|         Expanded( | ||||
|           child: MediaQuery.removePadding( | ||||
|             context: context, | ||||
| @@ -521,6 +445,7 @@ class _PostListWidgetState extends State<_PostListWidget> { | ||||
|               displacement: 40 + MediaQuery.of(context).padding.top, | ||||
|               onRefresh: () => refreshPosts(), | ||||
|               child: InfiniteList( | ||||
|                 padding: EdgeInsets.only(top: 8), | ||||
|                 itemCount: _posts.length, | ||||
|                 isLoading: _isBusy, | ||||
|                 centerLoading: true, | ||||
| @@ -542,18 +467,21 @@ class _PostListWidgetState extends State<_PostListWidget> { | ||||
|                 separatorBuilder: (_, __) => const Gap(8), | ||||
|               ), | ||||
|             ), | ||||
|           ).padding(top: 8), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostCategoryPickerPopup extends StatelessWidget { | ||||
|   final List<SnPostCategory> categories; | ||||
|   final SnPostCategory? selected; | ||||
| class _PostListRealmPopup extends StatelessWidget { | ||||
|   final List<SnRealm>? realms; | ||||
|   final Function(SnRealm?) onUpdate; | ||||
|  | ||||
|   const _PostCategoryPickerPopup({required this.categories, this.selected}); | ||||
|   const _PostListRealmPopup({ | ||||
|     required this.realms, | ||||
|     required this.onUpdate, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -563,62 +491,38 @@ class _PostCategoryPickerPopup extends StatelessWidget { | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const Icon(Symbols.category, size: 24), | ||||
|             const Icon(Symbols.face, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('postCategory') | ||||
|                 .tr() | ||||
|                 .textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|             Text('accountRealms', style: Theme.of(context).textTheme.titleLarge) | ||||
|                 .tr(), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         ListTile( | ||||
|           leading: const Icon(Symbols.clear), | ||||
|           title: Text('postFilterReset').tr(), | ||||
|           subtitle: Text('postFilterResetDescription').tr(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 20), | ||||
|           leading: const Icon(Symbols.close), | ||||
|           title: Text('postInGlobal').tr(), | ||||
|           subtitle: Text('postViewInGlobalDescription').tr(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           onTap: () { | ||||
|             Navigator.pop(context, false); | ||||
|             onUpdate.call(null); | ||||
|             Navigator.pop(context); | ||||
|           }, | ||||
|         ), | ||||
|         const Divider(height: 1), | ||||
|         Expanded( | ||||
|           child: GridView.count( | ||||
|             crossAxisCount: 4, | ||||
|             shrinkWrap: true, | ||||
|             physics: const NeverScrollableScrollPhysics(), | ||||
|             childAspectRatio: 1, | ||||
|             children: categories | ||||
|                 .map( | ||||
|                   (ele) => InkWell( | ||||
|                     onTap: () { | ||||
|                       _selectedCategory = ele; | ||||
|                       Navigator.pop(context, ele); | ||||
|                     }, | ||||
|                     child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       mainAxisSize: MainAxisSize.min, | ||||
|                       children: [ | ||||
|                         Icon( | ||||
|                           kCategoryIcons[ele.alias] ?? Symbols.question_mark, | ||||
|                           color: selected == ele | ||||
|                               ? Theme.of(context).colorScheme.primary | ||||
|                               : null, | ||||
|                         ), | ||||
|                         const Gap(4), | ||||
|                         Text( | ||||
|                           'postCategory${ele.alias.capitalize()}'.trExists() | ||||
|                               ? 'postCategory${ele.alias.capitalize()}'.tr() | ||||
|                               : ele.name, | ||||
|                         ) | ||||
|                             .textStyle(Theme.of(context).textTheme.titleMedium!) | ||||
|                             .textColor(selected == ele | ||||
|                                 ? Theme.of(context).colorScheme.primary | ||||
|                                 : null), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ) | ||||
|                 .toList(), | ||||
|           child: ListView.builder( | ||||
|             itemCount: realms?.length ?? 0, | ||||
|             itemBuilder: (context, idx) { | ||||
|               final realm = realms![idx]; | ||||
|               return ListTile( | ||||
|                 title: Text(realm.name), | ||||
|                 subtitle: Text('@${realm.alias}'), | ||||
|                 leading: AccountImage(content: realm.avatar, radius: 18), | ||||
|                 onTap: () { | ||||
|                   onUpdate.call(realm); | ||||
|                   Navigator.pop(context); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|   | ||||
							
								
								
									
										167
									
								
								lib/screens/logging.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								lib/screens/logging.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/logger.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:talker_dio_logger/dio_logs.dart'; | ||||
| import 'package:talker_flutter/talker_flutter.dart'; | ||||
|  | ||||
| final Map<LogLevel, IconData> kLogLevelIcons = { | ||||
|   LogLevel.error: Symbols.error, | ||||
|   LogLevel.critical: Symbols.error, | ||||
|   LogLevel.warning: Symbols.warning, | ||||
|   LogLevel.info: Symbols.info, | ||||
|   LogLevel.debug: Symbols.info_i, | ||||
|   LogLevel.verbose: Symbols.info_i, | ||||
| }; | ||||
|  | ||||
| final Map<LogLevel, bool> kLogLevelFilled = { | ||||
|   LogLevel.error: false, | ||||
|   LogLevel.critical: true, | ||||
|   LogLevel.warning: true, | ||||
|   LogLevel.info: true, | ||||
|   LogLevel.debug: false, | ||||
|   LogLevel.verbose: false, | ||||
| }; | ||||
|  | ||||
| class DebugLoggingScreen extends StatelessWidget { | ||||
|   const DebugLoggingScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final talkerTheme = TalkerScreenTheme.fromTheme(Theme.of(context)); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('debugLogging').tr(), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               logging.cleanHistory(); | ||||
|               Navigator.pop(context); | ||||
|             }, | ||||
|             icon: const Icon(Symbols.delete), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       body: ListView.builder( | ||||
|         reverse: true, | ||||
|         padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), | ||||
|         itemCount: logging.history.length, | ||||
|         itemBuilder: (context, index) { | ||||
|           final log = logging.history[index]; | ||||
|           final color = log.getFlutterColor(talkerTheme); | ||||
|           return ListTile( | ||||
|             minTileHeight: 0, | ||||
|             tileColor: color.withOpacity(0.2), | ||||
|             leading: Icon( | ||||
|               kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help, | ||||
|               color: color, | ||||
|               fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false) | ||||
|                   ? 1 | ||||
|                   : 0, | ||||
|             ), | ||||
|             title: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 if (log is DioRequestLog) | ||||
|                   Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         '${log.requestOptions.method} ${log.displayMessage}', | ||||
|                         style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                       ), | ||||
|                       if (log.requestOptions.data != null) | ||||
|                         Theme( | ||||
|                           data: Theme.of(context).copyWith( | ||||
|                             dividerColor: Colors.transparent, | ||||
|                           ), | ||||
|                           child: ExpansionTile( | ||||
|                             title: Text('Payload').fontSize(13), | ||||
|                             minTileHeight: 0, | ||||
|                             tilePadding: EdgeInsets.zero, | ||||
|                             expandedCrossAxisAlignment: | ||||
|                                 CrossAxisAlignment.start, | ||||
|                             children: [ | ||||
|                               Text( | ||||
|                                 log.requestOptions.data.toString(), | ||||
|                                 style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                     ], | ||||
|                   ) | ||||
|                 else if (log is DioResponseLog) | ||||
|                   Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         '${log.response.statusCode} ${log.displayMessage}', | ||||
|                         style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                       ), | ||||
|                       if (log.response.data != null) | ||||
|                         Theme( | ||||
|                           data: Theme.of(context).copyWith( | ||||
|                             dividerColor: Colors.transparent, | ||||
|                           ), | ||||
|                           child: ExpansionTile( | ||||
|                             title: Text('Payload').fontSize(13), | ||||
|                             minTileHeight: 0, | ||||
|                             tilePadding: EdgeInsets.zero, | ||||
|                             expandedCrossAxisAlignment: | ||||
|                                 CrossAxisAlignment.start, | ||||
|                             children: [ | ||||
|                               Text( | ||||
|                                 log.response.data.toString(), | ||||
|                                 style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                     ], | ||||
|                   ) | ||||
|                 else | ||||
|                   Text( | ||||
|                     log.displayMessage, | ||||
|                     style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                   ), | ||||
|                 if (log.exception != null) | ||||
|                   Text( | ||||
|                     log.displayException, | ||||
|                     style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                   ).bold(), | ||||
|                 if (log.error != null) | ||||
|                   Text( | ||||
|                     log.displayException, | ||||
|                     style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                   ).bold(), | ||||
|                 if (log.stackTrace != null) | ||||
|                   Text( | ||||
|                     log.displayStackTrace, | ||||
|                     style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                   ).padding(top: 4), | ||||
|               ], | ||||
|             ), | ||||
|             subtitle: Text( | ||||
|               '${(log.title?.replaceAll('-', ' ') ?? 'default').capitalizeEachWord()} · ${log.displayTime()}', | ||||
|             ).fontSize(11), | ||||
|             onTap: () { | ||||
|               Clipboard.setData( | ||||
|                 ClipboardData( | ||||
|                   text: log.generateTextMessage(), | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -29,6 +29,7 @@ const Map<String, IconData> kNotificationTopicIcons = { | ||||
|   'passport.security.otp': Symbols.password, | ||||
|   'interactive.subscription': Symbols.subscriptions, | ||||
|   'interactive.feedback': Symbols.add_reaction, | ||||
|   'interactive.reply': Symbols.reply, | ||||
|   'messaging.callStart': Symbols.call_received, | ||||
|   'wallet.transaction.new': Symbols.receipt, | ||||
| }; | ||||
| @@ -57,11 +58,12 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final nty = context.read<NotificationProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/id/notifications?take=10'); | ||||
|       _totalCount = resp.data['count']; | ||||
|       _notifications.addAll( | ||||
|         resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [], | ||||
|       final resp = await sn.client.get( | ||||
|         '/cgi/id/notifications', | ||||
|         queryParameters: {'take': 10, 'offset': _notifications.length}, | ||||
|       ); | ||||
|       _totalCount = resp.data['count']; | ||||
|       _notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? []); | ||||
|       nty.updateTray(); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
| @@ -96,9 +98,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|       nty.clear(); | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar( | ||||
|         'notificationMarkAllReadPrompt'.plural(resp.data['count']), | ||||
|       ); | ||||
|       context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count'])); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
| @@ -122,9 +122,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|       _fetchNotifications(); | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar( | ||||
|         'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']), | ||||
|       ); | ||||
|       context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}'])); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
| @@ -145,13 +143,8 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenNotification').tr(), | ||||
|         ), | ||||
|         body: Center( | ||||
|           child: UnauthorizedHint(), | ||||
|         ), | ||||
|         appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()), | ||||
|         body: Center(child: UnauthorizedHint()), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @@ -160,10 +153,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenNotification').tr(), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.checklist), | ||||
|             onPressed: _isSubmitting ? null : _markAllAsRead, | ||||
|           ), | ||||
|           IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead), | ||||
|           const Gap(8), | ||||
|         ], | ||||
|       ), | ||||
| @@ -177,10 +167,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|                 return _fetchNotifications(); | ||||
|               }, | ||||
|               child: InfiniteList( | ||||
|                 padding: EdgeInsets.only( | ||||
|                   top: 16, | ||||
|                   bottom: math.max(MediaQuery.of(context).padding.bottom, 16), | ||||
|                 ), | ||||
|                 padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)), | ||||
|                 itemCount: _notifications.length, | ||||
|                 onFetchData: () { | ||||
|                   _fetchNotifications(); | ||||
| @@ -199,41 +186,26 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             if (nty.readAt == null) | ||||
|                               StyledWidget(Badge( | ||||
|                                 label: Text('notificationUnread').tr(), | ||||
|                               )).padding(bottom: 4), | ||||
|                             Text( | ||||
|                               nty.title, | ||||
|                               style: Theme.of(context).textTheme.titleMedium, | ||||
|                             ), | ||||
|                               StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4), | ||||
|                             Text(nty.title, style: Theme.of(context).textTheme.titleMedium), | ||||
|                             if (nty.subtitle != null) | ||||
|                               Text( | ||||
|                                 nty.subtitle!, | ||||
|                                 style: Theme.of(context).textTheme.titleSmall, | ||||
|                               ), | ||||
|                               Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall), | ||||
|                             if (nty.subtitle != null) const Gap(4), | ||||
|                             SelectionArea( | ||||
|                               child: MarkdownTextContent( | ||||
|                                 content: nty.body, | ||||
|                                 isAutoWarp: true, | ||||
|                               ), | ||||
|                             ), | ||||
|                             if (['interactive.reply', 'interactive.feedback', 'interactive.subscription'] | ||||
|                                     .contains(nty.topic) && | ||||
|                             SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)), | ||||
|                             if ([ | ||||
|                                   'interactive.reply', | ||||
|                                   'interactive.feedback', | ||||
|                                   'interactive.subscription', | ||||
|                                 ].contains(nty.topic) && | ||||
|                                 nty.metadata['related_post'] != null) | ||||
|                               GestureDetector( | ||||
|                                 child: Container( | ||||
|                                   decoration: BoxDecoration( | ||||
|                                     borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                                     border: Border.all( | ||||
|                                       color: Theme.of(context).dividerColor, | ||||
|                                       width: 1, | ||||
|                                     ), | ||||
|                                     border: Border.all(color: Theme.of(context).dividerColor, width: 1), | ||||
|                                   ), | ||||
|                                   child: PostItem( | ||||
|                                     data: SnPost.fromJson( | ||||
|                                       nty.metadata['related_post']!, | ||||
|                                     ), | ||||
|                                     data: SnPost.fromJson(nty.metadata['related_post']!), | ||||
|                                     showComments: false, | ||||
|                                     showReactions: false, | ||||
|                                     showMenu: false, | ||||
| @@ -242,27 +214,18 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|                                 onTap: () { | ||||
|                                   GoRouter.of(context).pushNamed( | ||||
|                                     'postDetail', | ||||
|                                     pathParameters: { | ||||
|                                       'slug': nty.metadata['related_post']!['id'].toString(), | ||||
|                                     }, | ||||
|                                     pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()}, | ||||
|                                   ); | ||||
|                                 }, | ||||
|                               ).padding(top: 8), | ||||
|                             const Gap(8), | ||||
|                             Row( | ||||
|                               children: [ | ||||
|                                 Text( | ||||
|                                   DateFormat('yy/MM/dd').format(nty.createdAt), | ||||
|                                 ).fontSize(12), | ||||
|                                 Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12), | ||||
|                                 const Gap(4), | ||||
|                                 Text( | ||||
|                                   '·', | ||||
|                                   style: TextStyle(fontSize: 12), | ||||
|                                 ), | ||||
|                                 Text('·', style: TextStyle(fontSize: 12)), | ||||
|                                 const Gap(4), | ||||
|                                 Text( | ||||
|                                   RelativeTime(context).format(nty.createdAt), | ||||
|                                 ).fontSize(12), | ||||
|                                 Text(RelativeTime(context).format(nty.createdAt)).fontSize(12), | ||||
|                               ], | ||||
|                             ).opacity(0.75), | ||||
|                           ], | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/sn_realm.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| @@ -36,7 +37,8 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:surface/widgets/post/post_poll_editor.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| import '../../providers/sn_realm.dart'; | ||||
| const kPostTypes = ['Story', 'Article', 'Question', 'Video']; | ||||
| const kPostTypeAliases = ['stories', 'articles', 'questions', 'videos']; | ||||
|  | ||||
| class PostEditorExtra { | ||||
|   final String? text; | ||||
| @@ -53,7 +55,7 @@ class PostEditorExtra { | ||||
| } | ||||
|  | ||||
| class PostEditorScreen extends StatefulWidget { | ||||
|   final String mode; | ||||
|   final String? mode; | ||||
|   final int? postEditId; | ||||
|   final int? postReplyId; | ||||
|   final int? postRepostId; | ||||
| @@ -72,7 +74,10 @@ class PostEditorScreen extends StatefulWidget { | ||||
|   State<PostEditorScreen> createState() => _PostEditorScreenState(); | ||||
| } | ||||
|  | ||||
| class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
| class _PostEditorScreenState extends State<PostEditorScreen> | ||||
|     with SingleTickerProviderStateMixin { | ||||
|   late final TabController _tabController = | ||||
|       TabController(length: 4, vsync: this); | ||||
|   late final PostWriteController _writeController = PostWriteController( | ||||
|     doLoadFromTemporary: widget.postEditId == null, | ||||
|   ); | ||||
| @@ -95,8 +100,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|         resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], | ||||
|       ); | ||||
|       final beforeId = config.prefs.getInt('int_last_publisher_id'); | ||||
|       _writeController | ||||
|           .setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull); | ||||
|       _writeController.setPublisher( | ||||
|           _publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? | ||||
|               _publishers?.firstOrNull); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
| @@ -125,7 +131,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|  | ||||
|   final HotKey _pasteHotKey = HotKey( | ||||
|     key: PhysicalKeyboardKey.keyV, | ||||
|     modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control], | ||||
|     modifiers: [ | ||||
|       (!kIsWeb && Platform.isMacOS) | ||||
|           ? HotKeyModifier.meta | ||||
|           : HotKeyModifier.control | ||||
|     ], | ||||
|     scope: HotKeyScope.inapp, | ||||
|   ); | ||||
|  | ||||
| @@ -204,6 +214,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _tabController.dispose(); | ||||
|     _writeController.dispose(); | ||||
|     if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) { | ||||
|       hotKeyManager.unregister(_pasteHotKey); | ||||
| @@ -215,14 +226,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _registerHotKey(); | ||||
|     if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) { | ||||
|       context.showErrorDialog('Unknown post type'); | ||||
|       Navigator.pop(context); | ||||
|     } else { | ||||
|       _writeController.setMode(widget.mode); | ||||
|     } | ||||
|     _fetchRealms(); | ||||
|     _fetchPublishers(); | ||||
|     if (widget.mode != null) { | ||||
|       _writeController.setMode(widget.mode!); | ||||
|     } | ||||
|     _tabController.addListener(() { | ||||
|       if (_tabController.indexIsChanging) { | ||||
|         _writeController.setMode(kPostTypeAliases[_tabController.index]); | ||||
|       } | ||||
|     }); | ||||
|     _writeController.fetchRelatedPost( | ||||
|       context, | ||||
|       editing: widget.postEditId, | ||||
| @@ -232,7 +245,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     if (widget.extraProps != null) { | ||||
|       _writeController.contentController.text = widget.extraProps!.text ?? ''; | ||||
|       _writeController.titleController.text = widget.extraProps!.title ?? ''; | ||||
|       _writeController.descriptionController.text = widget.extraProps!.description ?? ''; | ||||
|       _writeController.descriptionController.text = | ||||
|           widget.extraProps!.description ?? ''; | ||||
|       _writeController.addAttachments(widget.extraProps!.attachments ?? []); | ||||
|     } | ||||
|   } | ||||
| @@ -249,24 +263,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                 Navigator.pop(context); | ||||
|               }, | ||||
|             ), | ||||
|             title: RichText( | ||||
|               textAlign: TextAlign.center, | ||||
|               text: TextSpan(children: [ | ||||
|                 TextSpan( | ||||
|                   text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(), | ||||
|                   style: Theme.of(context).textTheme.titleLarge!.copyWith( | ||||
|                         color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                       ), | ||||
|                 ), | ||||
|                 const TextSpan(text: '\n'), | ||||
|                 TextSpan( | ||||
|                   text: PostWriteController.kTitleMap[widget.mode]!.tr(), | ||||
|                   style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                         color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                       ), | ||||
|                 ), | ||||
|               ]), | ||||
|               maxLines: 2, | ||||
|             title: Text( | ||||
|               _writeController.title.isNotEmpty | ||||
|                   ? _writeController.title | ||||
|                   : 'untitled'.tr(), | ||||
|             ), | ||||
|             actions: [ | ||||
|               IconButton( | ||||
| @@ -275,12 +275,31 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|               ), | ||||
|               const Gap(8), | ||||
|             ], | ||||
|             bottom: _writeController.isNotEmpty || widget.mode != null | ||||
|                 ? null | ||||
|                 : TabBar( | ||||
|                     controller: _tabController, | ||||
|                     tabs: [ | ||||
|                       for (final type in kPostTypes) | ||||
|                         Tab( | ||||
|                           child: Text( | ||||
|                             'postType$type'.tr(), | ||||
|                             style: TextStyle( | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor!, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                     ], | ||||
|                   ), | ||||
|           ), | ||||
|           body: Column( | ||||
|             children: [ | ||||
|               if (_writeController.editingPost != null) | ||||
|                 Container( | ||||
|                   padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20), | ||||
|                   padding: const EdgeInsets.only( | ||||
|                       top: 4, bottom: 4, left: 20, right: 20), | ||||
|                   decoration: BoxDecoration( | ||||
|                     border: Border( | ||||
|                       bottom: BorderSide( | ||||
| @@ -294,13 +313,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                     children: [ | ||||
|                       const Icon(Icons.edit, size: 16), | ||||
|                       const Gap(10), | ||||
|                       Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']), | ||||
|                       Text('postEditingNotice').tr(args: [ | ||||
|                         '@${_writeController.editingPost!.publisher.name}' | ||||
|                       ]), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               if (_writeController.replyingPost != null) | ||||
|                 Container( | ||||
|                   padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20), | ||||
|                   padding: const EdgeInsets.only( | ||||
|                       top: 4, bottom: 4, left: 20, right: 20), | ||||
|                   decoration: BoxDecoration( | ||||
|                     border: Border( | ||||
|                       bottom: BorderSide( | ||||
| @@ -314,7 +336,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.reply, size: 16), | ||||
|                       const Gap(10), | ||||
|                       Text('@${_writeController.replyingPost!.publisher.name}').bold(), | ||||
|                       Text('@${_writeController.replyingPost!.publisher.name}') | ||||
|                           .bold(), | ||||
|                       const Gap(4), | ||||
|                       Expanded( | ||||
|                         child: Text( | ||||
| @@ -328,7 +351,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                 ), | ||||
|               if (_writeController.repostingPost != null) | ||||
|                 Container( | ||||
|                   padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20), | ||||
|                   padding: const EdgeInsets.only( | ||||
|                       top: 4, bottom: 4, left: 20, right: 20), | ||||
|                   decoration: BoxDecoration( | ||||
|                     border: Border( | ||||
|                       bottom: BorderSide( | ||||
| @@ -342,7 +366,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.forward, size: 16), | ||||
|                       const Gap(10), | ||||
|                       Text('@${_writeController.repostingPost!.publisher.name}').bold(), | ||||
|                       Text('@${_writeController.repostingPost!.publisher.name}') | ||||
|                           .bold(), | ||||
|                       const Gap(4), | ||||
|                       Expanded( | ||||
|                         child: Text( | ||||
| @@ -359,7 +384,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                   children: [ | ||||
|                     SingleChildScrollView( | ||||
|                       padding: EdgeInsets.only(bottom: 160), | ||||
|                       child: StyledWidget(switch (_writeController.mode) { | ||||
|                       child: switch (_writeController.mode) { | ||||
|                         'stories' => _PostStoryEditor( | ||||
|                             controller: _writeController, | ||||
|                             onTapPublisher: _showPublisherPopup, | ||||
| @@ -381,10 +406,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                             onTapRealm: _showRealmPopup, | ||||
|                           ), | ||||
|                         _ => const Placeholder(), | ||||
|                       }) | ||||
|                           .padding(top: 8), | ||||
|                       }, | ||||
|                     ), | ||||
|                     if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) | ||||
|                     if (_writeController.attachments.isNotEmpty || | ||||
|                         _writeController.thumbnail != null) | ||||
|                       Positioned( | ||||
|                         bottom: 0, | ||||
|                         left: 0, | ||||
| @@ -393,16 +418,19 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                           attachments: _writeController.attachments, | ||||
|                           isBusy: _writeController.isBusy, | ||||
|                           onUpload: (int idx) async { | ||||
|                             await _writeController.uploadSingleAttachment(context, idx); | ||||
|                             await _writeController.uploadSingleAttachment( | ||||
|                                 context, idx); | ||||
|                           }, | ||||
|                           onInsertLink: (int idx) async { | ||||
|                             _writeController.contentController.text += | ||||
|                                 '\n'; | ||||
|                           }, | ||||
|                           onUpdate: (int idx, PostWriteMedia updatedMedia) async { | ||||
|                           onUpdate: | ||||
|                               (int idx, PostWriteMedia updatedMedia) async { | ||||
|                             _writeController.setIsBusy(true); | ||||
|                             try { | ||||
|                               _writeController.setAttachmentAt(idx, updatedMedia); | ||||
|                               _writeController.setAttachmentAt( | ||||
|                                   idx, updatedMedia); | ||||
|                             } finally { | ||||
|                               _writeController.setIsBusy(false); | ||||
|                             } | ||||
| @@ -415,7 +443,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                               _writeController.setIsBusy(false); | ||||
|                             } | ||||
|                           }, | ||||
|                           onUpdateBusy: (state) => _writeController.setIsBusy(state), | ||||
|                           onUpdateBusy: (state) => | ||||
|                               _writeController.setIsBusy(state), | ||||
|                         ).padding(bottom: 8), | ||||
|                       ), | ||||
|                   ], | ||||
| @@ -426,11 +455,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     if (_writeController.isBusy && _writeController.progress != null) | ||||
|                     if (_writeController.isBusy && | ||||
|                         _writeController.progress != null) | ||||
|                       TweenAnimationBuilder<double>( | ||||
|                         tween: Tween(begin: 0, end: _writeController.progress), | ||||
|                         duration: Duration(milliseconds: 300), | ||||
|                         builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), | ||||
|                         builder: (context, value, _) => | ||||
|                             LinearProgressIndicator(value: value, minHeight: 2), | ||||
|                       ) | ||||
|                     else if (_writeController.isBusy) | ||||
|                       const LinearProgressIndicator(value: null, minHeight: 2), | ||||
| @@ -439,12 +470,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                     Container( | ||||
|                       child: _writeController.temporaryRestored | ||||
|                           ? Container( | ||||
|                               padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22), | ||||
|                               padding: const EdgeInsets.only( | ||||
|                                   top: 4, bottom: 4, left: 28, right: 22), | ||||
|                               decoration: BoxDecoration( | ||||
|                                 border: Border( | ||||
|                                   bottom: BorderSide( | ||||
|                                     color: Theme.of(context).dividerColor, | ||||
|                                     width: 1 / MediaQuery.of(context).devicePixelRatio, | ||||
|                                     width: 1 / | ||||
|                                         MediaQuery.of(context).devicePixelRatio, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
| @@ -453,7 +486,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                                 children: [ | ||||
|                                   const Icon(Icons.restore, size: 20), | ||||
|                                   const Gap(8), | ||||
|                                   Expanded(child: Text('postLocalDraftRestored').tr()), | ||||
|                                   Expanded( | ||||
|                                       child: | ||||
|                                           Text('postLocalDraftRestored').tr()), | ||||
|                                   InkWell( | ||||
|                                     child: Text('dialogDismiss').tr(), | ||||
|                                     onTap: () { | ||||
| @@ -464,8 +499,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                               )) | ||||
|                           : const SizedBox.shrink(), | ||||
|                     ) | ||||
|                         .height(_writeController.temporaryRestored ? 32 : 0, animate: true) | ||||
|                         .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn), | ||||
|                         .height(_writeController.temporaryRestored ? 32 : 0, | ||||
|                             animate: true) | ||||
|                         .animate(const Duration(milliseconds: 300), | ||||
|                             Curves.fastLinearToSlowEaseIn), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                       children: [ | ||||
| @@ -485,11 +522,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                                   ), | ||||
|                                   if (_writeController.mode == 'stories') | ||||
|                                     IconButton( | ||||
|                                       icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary), | ||||
|                                       icon: Icon(Symbols.poll, | ||||
|                                           color: Theme.of(context) | ||||
|                                               .colorScheme | ||||
|                                               .primary), | ||||
|                                       style: ButtonStyle( | ||||
|                                         backgroundColor: _writeController.poll == null | ||||
|                                             ? null | ||||
|                                             : WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer), | ||||
|                                         backgroundColor: | ||||
|                                             _writeController.poll == null | ||||
|                                                 ? null | ||||
|                                                 : WidgetStatePropertyAll( | ||||
|                                                     Theme.of(context) | ||||
|                                                         .colorScheme | ||||
|                                                         .surfaceContainer), | ||||
|                                       ), | ||||
|                                       onPressed: () { | ||||
|                                         _showPollEditorDialog(); | ||||
| @@ -497,14 +541,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                                     ), | ||||
|                                   if (_writeController.mode == 'articles') | ||||
|                                     IconButton( | ||||
|                                       icon: Icon(Symbols.full_coverage, color: Theme.of(context).colorScheme.primary), | ||||
|                                       icon: Icon(Symbols.full_coverage, | ||||
|                                           color: Theme.of(context) | ||||
|                                               .colorScheme | ||||
|                                               .primary), | ||||
|                                       style: ButtonStyle( | ||||
|                                         backgroundColor: _writeController.thumbnail == null | ||||
|                                             ? null | ||||
|                                             : WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer), | ||||
|                                         backgroundColor: | ||||
|                                             _writeController.thumbnail == null | ||||
|                                                 ? null | ||||
|                                                 : WidgetStatePropertyAll( | ||||
|                                                     Theme.of(context) | ||||
|                                                         .colorScheme | ||||
|                                                         .surfaceContainer), | ||||
|                                       ), | ||||
|                                       onPressed: () { | ||||
|                                         if (_writeController.thumbnail != null) { | ||||
|                                         if (_writeController.thumbnail != | ||||
|                                             null) { | ||||
|                                           _writeController.setThumbnail(null); | ||||
|                                           return; | ||||
|                                         } | ||||
| @@ -517,7 +569,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                           ), | ||||
|                         ), | ||||
|                         TextButton.icon( | ||||
|                           onPressed: (_writeController.isBusy || _writeController.publisher == null) | ||||
|                           onPressed: (_writeController.isBusy || | ||||
|                                   _writeController.publisher == null) | ||||
|                               ? null | ||||
|                               : () { | ||||
|                                   _writeController.sendPost(context).then((_) { | ||||
| @@ -556,7 +609,8 @@ class _PostPublisherPopup extends StatelessWidget { | ||||
|   final List<SnPublisher>? publishers; | ||||
|   final Function onUpdate; | ||||
|  | ||||
|   const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate}); | ||||
|   const _PostPublisherPopup( | ||||
|       {required this.controller, this.publishers, required this.onUpdate}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -568,7 +622,9 @@ class _PostPublisherPopup extends StatelessWidget { | ||||
|           children: [ | ||||
|             const Icon(Symbols.face, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||
|             Text('accountPublishers', | ||||
|                     style: Theme.of(context).textTheme.titleLarge) | ||||
|                 .tr(), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         ListTile( | ||||
| @@ -612,7 +668,8 @@ class _PostRealmPopup extends StatelessWidget { | ||||
|   final List<SnRealm>? realms; | ||||
|   final Function onUpdate; | ||||
|  | ||||
|   const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate}); | ||||
|   const _PostRealmPopup( | ||||
|       {required this.controller, this.realms, required this.onUpdate}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -624,7 +681,8 @@ class _PostRealmPopup extends StatelessWidget { | ||||
|           children: [ | ||||
|             const Icon(Symbols.face, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('accountRealms', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||
|             Text('accountRealms', style: Theme.of(context).textTheme.titleLarge) | ||||
|                 .tr(), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         ListTile( | ||||
| @@ -665,12 +723,13 @@ class _PostStoryEditor extends StatelessWidget { | ||||
|   final Function? onTapPublisher; | ||||
|   final Function? onTapRealm; | ||||
|  | ||||
|   const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|   const _PostStoryEditor( | ||||
|       {required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 12), | ||||
|       padding: const EdgeInsets.only(left: 12, right: 12, top: 8), | ||||
|       constraints: const BoxConstraints(maxWidth: 640), | ||||
|       child: Row( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
| @@ -717,7 +776,8 @@ class _PostStoryEditor extends StatelessWidget { | ||||
|                     border: InputBorder.none, | ||||
|                   ), | ||||
|                   style: Theme.of(context).textTheme.titleLarge, | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ).padding(horizontal: 16), | ||||
|                 const Gap(8), | ||||
|                 TextField( | ||||
| @@ -732,8 +792,10 @@ class _PostStoryEditor extends StatelessWidget { | ||||
|                     ), | ||||
|                     border: InputBorder.none, | ||||
|                   ), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   contentInsertionConfiguration: controller.contentInsertionConfiguration, | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   contentInsertionConfiguration: | ||||
|                       controller.contentInsertionConfiguration, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
| @@ -749,7 +811,8 @@ class _PostArticleEditor extends StatelessWidget { | ||||
|   final Function? onTapPublisher; | ||||
|   final Function? onTapRealm; | ||||
|  | ||||
|   const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|   const _PostArticleEditor( | ||||
|       {required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -857,8 +920,10 @@ class _PostArticleEditor extends StatelessWidget { | ||||
|                       ), | ||||
|                       border: InputBorder.none, | ||||
|                     ), | ||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     contentInsertionConfiguration: controller.contentInsertionConfiguration, | ||||
|                     onTapOutside: (_) => | ||||
|                         FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     contentInsertionConfiguration: | ||||
|                         controller.contentInsertionConfiguration, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const Gap(8), | ||||
| @@ -893,7 +958,8 @@ class _PostArticleEditor extends StatelessWidget { | ||||
|               border: InputBorder.none, | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             contentInsertionConfiguration: controller.contentInsertionConfiguration, | ||||
|             contentInsertionConfiguration: | ||||
|                 controller.contentInsertionConfiguration, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
| @@ -906,12 +972,13 @@ class _PostQuestionEditor extends StatelessWidget { | ||||
|   final Function? onTapPublisher; | ||||
|   final Function? onTapRealm; | ||||
|  | ||||
|   const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|   const _PostQuestionEditor( | ||||
|       {required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 12), | ||||
|       padding: const EdgeInsets.only(left: 12, right: 12, top: 8), | ||||
|       constraints: const BoxConstraints(maxWidth: 640), | ||||
|       child: Row( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
| @@ -958,7 +1025,8 @@ class _PostQuestionEditor extends StatelessWidget { | ||||
|                     border: InputBorder.none, | ||||
|                   ), | ||||
|                   style: Theme.of(context).textTheme.titleLarge, | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ).padding(horizontal: 16), | ||||
|                 const Gap(8), | ||||
|                 TextField( | ||||
| @@ -969,7 +1037,8 @@ class _PostQuestionEditor extends StatelessWidget { | ||||
|                     border: InputBorder.none, | ||||
|                     isCollapsed: true, | ||||
|                   ), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ).padding(horizontal: 16), | ||||
|                 const Gap(8), | ||||
|                 TextField( | ||||
| @@ -984,14 +1053,16 @@ class _PostQuestionEditor extends StatelessWidget { | ||||
|                     ), | ||||
|                     border: InputBorder.none, | ||||
|                   ), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   contentInsertionConfiguration: controller.contentInsertionConfiguration, | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   contentInsertionConfiguration: | ||||
|                       controller.contentInsertionConfiguration, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ).padding(top: 8), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1001,7 +1072,8 @@ class _PostVideoEditor extends StatelessWidget { | ||||
|   final Function? onTapPublisher; | ||||
|   final Function? onTapRealm; | ||||
|  | ||||
|   const _PostVideoEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|   const _PostVideoEditor( | ||||
|       {required this.controller, this.onTapPublisher, this.onTapRealm}); | ||||
|  | ||||
|   void _selectVideo(BuildContext context) async { | ||||
|     final video = await showDialog<SnAttachment?>( | ||||
| @@ -1022,7 +1094,8 @@ class _PostVideoEditor extends StatelessWidget { | ||||
|  | ||||
|     final result = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)), | ||||
|       builder: (context) => PendingAttachmentAltDialog( | ||||
|           media: PostWriteMedia(controller.videoAttachment)), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
| @@ -1034,7 +1107,8 @@ class _PostVideoEditor extends StatelessWidget { | ||||
|  | ||||
|     final result = await showDialog<SnAttachmentBoost?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)), | ||||
|       builder: (context) => PendingAttachmentBoostDialog( | ||||
|           media: PostWriteMedia(controller.videoAttachment)), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
| @@ -1077,7 +1151,8 @@ class _PostVideoEditor extends StatelessWidget { | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}'); | ||||
|       await sn.client | ||||
|           .delete('/cgi/uc/attachments/${controller.videoAttachment!.id}'); | ||||
|       controller.setVideoAttachment(null); | ||||
|     } catch (err) { | ||||
|       if (!context.mounted) return; | ||||
| @@ -1087,143 +1162,159 @@ class _PostVideoEditor extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Column( | ||||
|       children: [ | ||||
|         Column( | ||||
|           children: [ | ||||
|             Material( | ||||
|               elevation: 2, | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(24)), | ||||
|               child: GestureDetector( | ||||
|                 onTap: () { | ||||
|                   onTapPublisher?.call(); | ||||
|                 }, | ||||
|                 child: AccountImage( | ||||
|                   content: controller.publisher?.avatar, | ||||
|     return Container( | ||||
|       padding: const EdgeInsets.only(left: 12, right: 12, top: 8), | ||||
|       constraints: const BoxConstraints(maxWidth: 640), | ||||
|       child: Row( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Column( | ||||
|             children: [ | ||||
|               Material( | ||||
|                 elevation: 2, | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(24)), | ||||
|                 child: GestureDetector( | ||||
|                   onTap: () { | ||||
|                     onTapPublisher?.call(); | ||||
|                   }, | ||||
|                   child: AccountImage( | ||||
|                     content: controller.publisher?.avatar, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             const Gap(11), | ||||
|             Material( | ||||
|               elevation: 1, | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(24)), | ||||
|               child: GestureDetector( | ||||
|                 onTap: () { | ||||
|                   onTapRealm?.call(); | ||||
|                 }, | ||||
|                 child: AccountImage( | ||||
|                   content: controller.realm?.avatar, | ||||
|                   fallbackWidget: const Icon(Symbols.globe, size: 20), | ||||
|                   radius: 14, | ||||
|               const Gap(11), | ||||
|               Material( | ||||
|                 elevation: 1, | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(24)), | ||||
|                 child: GestureDetector( | ||||
|                   onTap: () { | ||||
|                     onTapRealm?.call(); | ||||
|                   }, | ||||
|                   child: AccountImage( | ||||
|                     content: controller.realm?.avatar, | ||||
|                     fallbackWidget: const Icon(Symbols.globe, size: 20), | ||||
|                     radius: 14, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         const Gap(16), | ||||
|         TextField( | ||||
|           controller: controller.titleController, | ||||
|           decoration: InputDecoration.collapsed( | ||||
|             hintText: 'fieldPostTitle'.tr(), | ||||
|             border: InputBorder.none, | ||||
|             ], | ||||
|           ), | ||||
|           style: Theme.of(context).textTheme.titleLarge, | ||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|         ).padding(horizontal: 16), | ||||
|         const Gap(8), | ||||
|         TextField( | ||||
|           controller: controller.descriptionController, | ||||
|           decoration: InputDecoration.collapsed( | ||||
|             hintText: 'fieldPostDescription'.tr(), | ||||
|             border: InputBorder.none, | ||||
|           ), | ||||
|           maxLines: null, | ||||
|           keyboardType: TextInputType.multiline, | ||||
|           style: Theme.of(context).textTheme.bodyLarge, | ||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|         ).padding(horizontal: 16), | ||||
|         const Gap(12), | ||||
|         Container( | ||||
|           margin: const EdgeInsets.only(left: 16, right: 16), | ||||
|           decoration: BoxDecoration( | ||||
|             borderRadius: BorderRadius.circular(16), | ||||
|             border: Border.all(color: Theme.of(context).dividerColor), | ||||
|           ), | ||||
|           child: ContextMenuRegion( | ||||
|             contextMenu: ContextMenu( | ||||
|               entries: [ | ||||
|                 MenuItem( | ||||
|                   label: 'attachmentSetAlt'.tr(), | ||||
|                   icon: Symbols.description, | ||||
|                   onSelected: () { | ||||
|                     _setAlt(context); | ||||
|                   }, | ||||
|                 ), | ||||
|                 MenuItem( | ||||
|                   label: 'attachmentBoost'.tr(), | ||||
|                   icon: Symbols.bolt, | ||||
|                   onSelected: () { | ||||
|                     _createBoost(context); | ||||
|                   }, | ||||
|                 ), | ||||
|                 MenuItem( | ||||
|                   label: 'attachmentSetThumbnail'.tr(), | ||||
|                   icon: Symbols.image, | ||||
|                   onSelected: () { | ||||
|                     _setThumbnail(context); | ||||
|                   }, | ||||
|                 ), | ||||
|                 MenuItem( | ||||
|                   label: 'attachmentCopyRandomId'.tr(), | ||||
|                   icon: Symbols.content_copy, | ||||
|                   onSelected: () { | ||||
|                     Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid)); | ||||
|                   }, | ||||
|                 ), | ||||
|                 MenuItem( | ||||
|                   label: 'delete'.tr(), | ||||
|                   icon: Symbols.delete, | ||||
|                   onSelected: () => _deleteAttachment(context), | ||||
|                 ), | ||||
|                 MenuItem( | ||||
|                   label: 'unlink'.tr(), | ||||
|                   icon: Symbols.link_off, | ||||
|                   onSelected: () { | ||||
|                     controller.setVideoAttachment(null); | ||||
|                   }, | ||||
|           Expanded( | ||||
|             child: Column( | ||||
|               children: [ | ||||
|                 const Gap(6), | ||||
|                 TextField( | ||||
|                   controller: controller.titleController, | ||||
|                   decoration: InputDecoration.collapsed( | ||||
|                     hintText: 'fieldPostTitle'.tr(), | ||||
|                     border: InputBorder.none, | ||||
|                   ), | ||||
|                   style: Theme.of(context).textTheme.titleLarge, | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ).padding(horizontal: 16), | ||||
|                 const Gap(8), | ||||
|                 TextField( | ||||
|                   controller: controller.descriptionController, | ||||
|                   decoration: InputDecoration.collapsed( | ||||
|                     hintText: 'fieldPostDescription'.tr(), | ||||
|                     border: InputBorder.none, | ||||
|                   ), | ||||
|                   maxLines: null, | ||||
|                   keyboardType: TextInputType.multiline, | ||||
|                   style: Theme.of(context).textTheme.bodyLarge, | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ).padding(horizontal: 16), | ||||
|                 const Gap(12), | ||||
|                 Container( | ||||
|                   margin: const EdgeInsets.only(left: 16, right: 16), | ||||
|                   decoration: BoxDecoration( | ||||
|                     borderRadius: BorderRadius.circular(16), | ||||
|                     border: Border.all(color: Theme.of(context).dividerColor), | ||||
|                   ), | ||||
|                   child: ContextMenuRegion( | ||||
|                     contextMenu: ContextMenu( | ||||
|                       entries: [ | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentSetAlt'.tr(), | ||||
|                           icon: Symbols.description, | ||||
|                           onSelected: () { | ||||
|                             _setAlt(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentBoost'.tr(), | ||||
|                           icon: Symbols.bolt, | ||||
|                           onSelected: () { | ||||
|                             _createBoost(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentSetThumbnail'.tr(), | ||||
|                           icon: Symbols.image, | ||||
|                           onSelected: () { | ||||
|                             _setThumbnail(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentCopyRandomId'.tr(), | ||||
|                           icon: Symbols.content_copy, | ||||
|                           onSelected: () { | ||||
|                             Clipboard.setData(ClipboardData( | ||||
|                                 text: controller.videoAttachment!.rid)); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'delete'.tr(), | ||||
|                           icon: Symbols.delete, | ||||
|                           onSelected: () => _deleteAttachment(context), | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'unlink'.tr(), | ||||
|                           icon: Symbols.link_off, | ||||
|                           onSelected: () { | ||||
|                             controller.setVideoAttachment(null); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     child: InkWell( | ||||
|                       borderRadius: BorderRadius.circular(16), | ||||
|                       onTap: controller.videoAttachment == null | ||||
|                           ? () => _selectVideo(context) | ||||
|                           : null, | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 16 / 9, | ||||
|                         child: controller.videoAttachment == null | ||||
|                             ? Center( | ||||
|                                 child: Row( | ||||
|                                   mainAxisSize: MainAxisSize.min, | ||||
|                                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                                   children: [ | ||||
|                                     const Icon(Icons.add), | ||||
|                                     const Gap(4), | ||||
|                                     Text('postVideoUpload'.tr()), | ||||
|                                   ], | ||||
|                                 ), | ||||
|                               ) | ||||
|                             : ClipRRect( | ||||
|                                 borderRadius: BorderRadius.circular(16), | ||||
|                                 child: AttachmentItem( | ||||
|                                   data: controller.videoAttachment!, | ||||
|                                   heroTag: const Uuid().v4(), | ||||
|                                 ), | ||||
|                               ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             child: InkWell( | ||||
|               borderRadius: BorderRadius.circular(16), | ||||
|               onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null, | ||||
|               child: AspectRatio( | ||||
|                 aspectRatio: 16 / 9, | ||||
|                 child: controller.videoAttachment == null | ||||
|                     ? Center( | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                           mainAxisAlignment: MainAxisAlignment.center, | ||||
|                           children: [ | ||||
|                             const Icon(Icons.add), | ||||
|                             const Gap(4), | ||||
|                             Text('postVideoUpload'.tr()), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ) | ||||
|                     : ClipRRect( | ||||
|                         borderRadius: BorderRadius.circular(16), | ||||
|                         child: AttachmentItem( | ||||
|                           data: controller.videoAttachment!, | ||||
|                           heroTag: const Uuid().v4(), | ||||
|                         ), | ||||
|                       ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -51,7 +51,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> { | ||||
|   Future<void> _fetchPublishers() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/co/publishers?realm=${widget.alias}'); | ||||
|       final resp = | ||||
|           await sn.client.get('/cgi/co/publishers?realm=${widget.alias}'); | ||||
|       _publishers = List<SnPublisher>.from( | ||||
|         resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], | ||||
|       ); | ||||
| @@ -68,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> { | ||||
|   Future<void> _fetchChannels() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public'); | ||||
|       final resp = | ||||
|           await sn.client.get('/cgi/im/channels/${widget.alias}/public'); | ||||
|       _channels = List<SnChannel>.from( | ||||
|         resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(), | ||||
|       ); | ||||
| @@ -98,15 +100,32 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> { | ||||
|           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|             return <Widget>[ | ||||
|               SliverOverlapAbsorber( | ||||
|                 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||
|                 handle: | ||||
|                     NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||
|                 sliver: SliverAppBar( | ||||
|                   title: Text(_realm?.name ?? 'loading'.tr()), | ||||
|                   bottom: TabBar( | ||||
|                     tabs: [ | ||||
|                       Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)), | ||||
|                       Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)), | ||||
|                       Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)), | ||||
|                       Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)), | ||||
|                       Tab( | ||||
|                           icon: Icon(Symbols.home, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor)), | ||||
|                       Tab( | ||||
|                           icon: Icon(Symbols.explore, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor)), | ||||
|                       Tab( | ||||
|                           icon: Icon(Symbols.group, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor)), | ||||
|                       Tab( | ||||
|                           icon: Icon(Symbols.settings, | ||||
|                               color: Theme.of(context) | ||||
|                                   .appBarTheme | ||||
|                                   .foregroundColor)), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
| @@ -115,7 +134,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> { | ||||
|           }, | ||||
|           body: TabBarView( | ||||
|             children: [ | ||||
|               _RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels), | ||||
|               _RealmDetailHomeWidget( | ||||
|                   realm: _realm, publishers: _publishers, channels: _channels), | ||||
|               _RealmPostListWidget(realm: _realm), | ||||
|               _RealmMemberListWidget(realm: _realm), | ||||
|               _RealmSettingsWidget( | ||||
| @@ -137,7 +157,8 @@ class _RealmDetailHomeWidget extends StatelessWidget { | ||||
|   final List<SnPublisher>? publishers; | ||||
|   final List<SnChannel>? channels; | ||||
|  | ||||
|   const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels}); | ||||
|   const _RealmDetailHomeWidget( | ||||
|       {required this.realm, this.publishers, this.channels}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -168,7 +189,8 @@ class _RealmDetailHomeWidget extends StatelessWidget { | ||||
|                   child: Container( | ||||
|                     width: double.infinity, | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                     child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium) | ||||
|                     child: Text('realmCommunityPublishersHint'.tr(), | ||||
|                             style: Theme.of(context).textTheme.bodyMedium) | ||||
|                         .padding(horizontal: 24, vertical: 8), | ||||
|                   ), | ||||
|                 ), | ||||
| @@ -199,7 +221,8 @@ class _RealmDetailHomeWidget extends StatelessWidget { | ||||
|                   child: Container( | ||||
|                     width: double.infinity, | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                     child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium) | ||||
|                     child: Text('realmCommunityPublicChannelsHint'.tr(), | ||||
|                             style: Theme.of(context).textTheme.bodyMedium) | ||||
|                         .padding(horizontal: 24, vertical: 8), | ||||
|                   ), | ||||
|                 ), | ||||
| @@ -323,10 +346,12 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { | ||||
|     try { | ||||
|       final ud = context.read<UserDirectoryProvider>(); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: { | ||||
|         'take': 10, | ||||
|         'offset': _members.length, | ||||
|       }); | ||||
|       final resp = await sn.client.get( | ||||
|           '/cgi/id/realms/${widget.realm!.alias}/members', | ||||
|           queryParameters: { | ||||
|             'take': 10, | ||||
|             'offset': _members.length, | ||||
|           }); | ||||
|  | ||||
|       final out = List<SnRealmMember>.from( | ||||
|         resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [], | ||||
| @@ -432,14 +457,14 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { | ||||
|             return ListTile( | ||||
|               contentPadding: const EdgeInsets.only(right: 24, left: 16), | ||||
|               leading: AccountImage( | ||||
|                 content: ud.getAccountFromCache(member.accountId)?.avatar, | ||||
|                 content: ud.getFromCache(member.accountId)?.avatar, | ||||
|                 fallbackWidget: const Icon(Symbols.group, size: 24), | ||||
|               ), | ||||
|               title: Text( | ||||
|                 ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(), | ||||
|                 ud.getFromCache(member.accountId)?.nick ?? 'unknown'.tr(), | ||||
|               ), | ||||
|               subtitle: Text( | ||||
|                 ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(), | ||||
|                 ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(), | ||||
|               ), | ||||
|               trailing: IconButton( | ||||
|                 icon: const Icon(Symbols.person_remove), | ||||
|   | ||||
| @@ -6,6 +6,7 @@ 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_cache_manager/flutter_cache_manager.dart'; | ||||
| import 'package:flutter_colorpicker/flutter_colorpicker.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| @@ -48,6 +49,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|   late final SharedPreferences _prefs; | ||||
|   String _docBasepath = '/'; | ||||
|  | ||||
|   final TextEditingController _customFontController = TextEditingController(); | ||||
|   final TextEditingController _serverUrlController = TextEditingController(); | ||||
|  | ||||
|   @override | ||||
| @@ -62,11 +64,15 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|     final config = context.read<ConfigProvider>(); | ||||
|     _prefs = config.prefs; | ||||
|     _serverUrlController.text = config.serverUrl; | ||||
|     if (_prefs.getString(kAppCustomFonts) != null) { | ||||
|       _customFontController.text = _prefs.getString(kAppCustomFonts) ?? ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _serverUrlController.dispose(); | ||||
|     _customFontController.dispose(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| @@ -330,6 +336,47 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                     setState(() {}); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Symbols.font_download), | ||||
|                   title: Text('settingsCustomFonts').tr(), | ||||
|                   subtitle: Text('settingsCustomFontsDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 14), | ||||
|                   trailing: IconButton( | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     constraints: const BoxConstraints(), | ||||
|                     icon: const Icon(Icons.clear), | ||||
|                     onPressed: () { | ||||
|                       _prefs.remove(kAppCustomFonts); | ||||
|                       context.showSnackbar('settingsCustomFontApplied'.tr()); | ||||
|                       final theme = context.read<ThemeProvider>(); | ||||
|                       _customFontController.clear(); | ||||
|                       theme.reloadTheme(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|                 TextField( | ||||
|                   controller: _customFontController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     labelText: 'settingsCustomFontFamily'.tr(), | ||||
|                     helperText: 'settingsCustomFontFamilyHint'.tr(), | ||||
|                     prefixIcon: const Icon(Symbols.format_paint), | ||||
|                     suffixIcon: IconButton( | ||||
|                       icon: const Icon(Symbols.save), | ||||
|                       onPressed: () { | ||||
|                         _prefs.setString( | ||||
|                           kAppCustomFonts, | ||||
|                           _customFontController.text, | ||||
|                         ); | ||||
|                         context.showSnackbar('settingsCustomFontApplied'.tr()); | ||||
|                         final theme = context.read<ThemeProvider>(); | ||||
|                         theme.reloadTheme(); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ).padding(horizontal: 16, top: 8, bottom: 4), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
| @@ -534,6 +581,37 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                     .fontSize(17) | ||||
|                     .tr() | ||||
|                     .padding(horizontal: 20, bottom: 4), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Symbols.home_storage), | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('cacheSize').tr(), | ||||
|                   subtitle: FutureBuilder( | ||||
|                     future: DefaultCacheManager().store.getCacheSize(), | ||||
|                     builder: (context, snapshot) { | ||||
|                       if (!snapshot.hasData || kIsWeb) { | ||||
|                         return Text('unknown').tr(); | ||||
|                       } | ||||
|                       return Text( | ||||
|                         snapshot.data!.formatBytes(), | ||||
|                         style: GoogleFonts.robotoMono(), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Symbols.cleaning_services), | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('cacheDelete').tr(), | ||||
|                   subtitle: Text('cacheDeleteDescription').tr(), | ||||
|                   trailing: const Icon(Symbols.chevron_right), | ||||
|                   onTap: () async { | ||||
|                     await DefaultCacheManager().emptyCache(); | ||||
|                     if (!context.mounted) return; | ||||
|                     HapticFeedback.heavyImpact(); | ||||
|                     context.showSnackbar('cacheDeleted'.tr()); | ||||
|                     setState(() {}); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Symbols.database), | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
| @@ -618,6 +696,16 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   title: Text('runtimeLogsOpen').tr(), | ||||
|                   subtitle: Text('runtimeLogsDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                   leading: const Icon(Symbols.receipt_long), | ||||
|                   trailing: const Icon(Symbols.chevron_right), | ||||
|                   onTap: () async { | ||||
|                     GoRouter.of(context).pushNamed('debugLogging'); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   title: Text('settingsMiscAbout').tr(), | ||||
|                   subtitle: Text('settingsMiscAboutDescription').tr(), | ||||
|   | ||||
| @@ -51,26 +51,35 @@ class _AppSharingListenerState extends State<AppSharingListener> { | ||||
|                 child: Column( | ||||
|                   children: [ | ||||
|                     ListTile( | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | ||||
|                       contentPadding: | ||||
|                           const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       shape: RoundedRectangleBorder( | ||||
|                           borderRadius: BorderRadius.circular(8)), | ||||
|                       leading: Icon(Icons.post_add), | ||||
|                       trailing: const Icon(Icons.chevron_right), | ||||
|                       title: Text('shareIntentPostStory').tr(), | ||||
|                       onTap: () { | ||||
|                         GoRouter.of(context).pushNamed( | ||||
|                           'postEditor', | ||||
|                           pathParameters: { | ||||
|                           queryParameters: { | ||||
|                             'mode': 'stories', | ||||
|                           }, | ||||
|                           extra: PostEditorExtra( | ||||
|                             text: value | ||||
|                                 .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) | ||||
|                                 .where((e) => [ | ||||
|                                       SharedMediaType.text, | ||||
|                                       SharedMediaType.url | ||||
|                                     ].contains(e.type)) | ||||
|                                 .map((e) => e.path) | ||||
|                                 .join('\n'), | ||||
|                             attachments: value | ||||
|                                 .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image] | ||||
|                                     .contains(e.type)) | ||||
|                                 .map((e) => PostWriteMedia.fromFile(XFile(e.path))) | ||||
|                                 .where((e) => [ | ||||
|                                       SharedMediaType.video, | ||||
|                                       SharedMediaType.file, | ||||
|                                       SharedMediaType.image | ||||
|                                     ].contains(e.type)) | ||||
|                                 .map((e) => | ||||
|                                     PostWriteMedia.fromFile(XFile(e.path))) | ||||
|                                 .toList(), | ||||
|                           ), | ||||
|                         ); | ||||
| @@ -78,15 +87,18 @@ class _AppSharingListenerState extends State<AppSharingListener> { | ||||
|                       }, | ||||
|                     ), | ||||
|                     ListTile( | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | ||||
|                       contentPadding: | ||||
|                           const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       shape: RoundedRectangleBorder( | ||||
|                           borderRadius: BorderRadius.circular(8)), | ||||
|                       leading: Icon(Icons.chat_outlined), | ||||
|                       trailing: const Icon(Icons.chevron_right), | ||||
|                       title: Text('shareIntentSendChannel').tr(), | ||||
|                       onTap: () { | ||||
|                         showModalBottomSheet( | ||||
|                           context: context, | ||||
|                           builder: (context) => _ShareIntentChannelSelect(value: value), | ||||
|                           builder: (context) => | ||||
|                               _ShareIntentChannelSelect(value: value), | ||||
|                         ).then((val) { | ||||
|                           if (!context.mounted) return; | ||||
|                           if (val == true) Navigator.pop(context); | ||||
| @@ -110,7 +122,8 @@ class _AppSharingListenerState extends State<AppSharingListener> { | ||||
|   } | ||||
|  | ||||
|   void _initialize() async { | ||||
|     _shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) { | ||||
|     _shareIntentSubscription = | ||||
|         ReceiveSharingIntent.instance.getMediaStream().listen((value) { | ||||
|       if (value.isEmpty) return; | ||||
|       if (mounted) { | ||||
|         _gotoPost(value); | ||||
| @@ -157,7 +170,8 @@ class _ShareIntentChannelSelect extends StatefulWidget { | ||||
|   const _ShareIntentChannelSelect({required this.value}); | ||||
|  | ||||
|   @override | ||||
|   State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState(); | ||||
|   State<_ShareIntentChannelSelect> createState() => | ||||
|       _ShareIntentChannelSelectState(); | ||||
| } | ||||
|  | ||||
| class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||
| @@ -178,8 +192,11 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||
|       final lastMessages = await chan.getLastMessages(channels); | ||||
|       _lastMessages = {for (final val in lastMessages) val.channelId: val}; | ||||
|       channels.sort((a, b) { | ||||
|         if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) { | ||||
|           return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt); | ||||
|         if (_lastMessages!.containsKey(a.id) && | ||||
|             _lastMessages!.containsKey(b.id)) { | ||||
|           return _lastMessages![b.id]! | ||||
|               .createdAt | ||||
|               .compareTo(_lastMessages![a.id]!.createdAt); | ||||
|         } | ||||
|         if (_lastMessages!.containsKey(a.id)) return -1; | ||||
|         if (_lastMessages!.containsKey(b.id)) return 1; | ||||
| @@ -232,7 +249,9 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||
|           children: [ | ||||
|             const Icon(Symbols.chat, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||
|             Text('shareIntentSendChannel', | ||||
|                     style: Theme.of(context).textTheme.titleLarge) | ||||
|                 .tr(), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         LoadingIndicator(isActive: _isBusy), | ||||
| @@ -249,29 +268,34 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||
|                   final lastMessage = _lastMessages?[channel.id]; | ||||
|  | ||||
|                   if (channel.type == 1) { | ||||
|                     final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( | ||||
|                           (ele) => ele?.accountId != ua.user?.id, | ||||
|                           orElse: () => null, | ||||
|                         ); | ||||
|                     final otherMember = | ||||
|                         channel.members?.cast<SnChannelMember?>().firstWhere( | ||||
|                               (ele) => ele?.accountId != ua.user?.id, | ||||
|                               orElse: () => null, | ||||
|                             ); | ||||
|  | ||||
|                     return ListTile( | ||||
|                       title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), | ||||
|                       title: Text( | ||||
|                           ud.getFromCache(otherMember?.accountId)?.nick ?? | ||||
|                               channel.name), | ||||
|                       subtitle: lastMessage != null | ||||
|                           ? Text( | ||||
|                               '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                               '${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ) | ||||
|                           : Text( | ||||
|                               'channelDirectMessageDescription'.tr(args: [ | ||||
|                                 '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', | ||||
|                                 '@${ud.getFromCache(otherMember?.accountId)?.name}', | ||||
|                               ]), | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ), | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       contentPadding: | ||||
|                           const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       leading: AccountImage( | ||||
|                         content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, | ||||
|                         content: | ||||
|                             ud.getFromCache(otherMember?.accountId)?.avatar, | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         GoRouter.of(context).pushNamed( | ||||
| @@ -291,7 +315,7 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||
|                     title: Text(channel.name), | ||||
|                     subtitle: lastMessage != null | ||||
|                         ? Text( | ||||
|                             '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                             '${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                             maxLines: 1, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                           ) | ||||
| @@ -316,13 +340,20 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||
|                         }, | ||||
|                         extra: ChatRoomScreenExtra( | ||||
|                           initialText: widget.value | ||||
|                               .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) | ||||
|                               .where((e) => [ | ||||
|                                     SharedMediaType.text, | ||||
|                                     SharedMediaType.url | ||||
|                                   ].contains(e.type)) | ||||
|                               .map((e) => e.path) | ||||
|                               .join('\n'), | ||||
|                           initialAttachments: widget.value | ||||
|                               .where((e) => | ||||
|                                   [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) | ||||
|                               .map((e) => PostWriteMedia.fromFile(XFile(e.path))) | ||||
|                               .where((e) => [ | ||||
|                                     SharedMediaType.video, | ||||
|                                     SharedMediaType.file, | ||||
|                                     SharedMediaType.image | ||||
|                                   ].contains(e.type)) | ||||
|                               .map( | ||||
|                                   (e) => PostWriteMedia.fromFile(XFile(e.path))) | ||||
|                               .toList(), | ||||
|                         ), | ||||
|                       ) | ||||
|   | ||||
| @@ -179,7 +179,9 @@ class _StickerScreenState extends State<StickerScreen> | ||||
|           child: InfiniteList( | ||||
|             itemCount: _packs.length, | ||||
|             onFetchData: _fetchPacks, | ||||
|             hasReachedMax: _totalCount != null && _packs.length >= _totalCount!, | ||||
|             hasReachedMax: | ||||
|                 (_totalCount != null && _packs.length >= _totalCount!) || | ||||
|                     _tabController.index == 2, | ||||
|             isLoading: _isBusy, | ||||
|             itemBuilder: (context, idx) { | ||||
|               final pack = _packs[idx]; | ||||
| @@ -282,7 +284,10 @@ class _StickerPackAddPopupState extends State<_StickerPackAddPopup> { | ||||
|       ); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('stickersAdded'.tr()); | ||||
|       if (_pack?.stickers != null) stickers.putSticker(_pack!.stickers!); | ||||
|       if (_pack?.stickers != null) { | ||||
|         stickers.putSticker( | ||||
|             _pack!.stickers!.map((ele) => ele.copyWith(pack: _pack!))); | ||||
|       } | ||||
|       Navigator.pop(context, true); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|   | ||||
| @@ -11,10 +11,19 @@ class ThemeSet { | ||||
|   ThemeSet({required this.light, required this.dark}); | ||||
| } | ||||
|  | ||||
| Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async { | ||||
| Future<ThemeSet> createAppThemeSet( | ||||
|     {Color? seedColorOverride, bool? useMaterial3, String? customFonts}) async { | ||||
|   return ThemeSet( | ||||
|     light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3), | ||||
|     dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3), | ||||
|     light: await createAppTheme( | ||||
|       Brightness.light, | ||||
|       useMaterial3: useMaterial3, | ||||
|       customFonts: customFonts, | ||||
|     ), | ||||
|     dark: await createAppTheme( | ||||
|       Brightness.dark, | ||||
|       useMaterial3: useMaterial3, | ||||
|       customFonts: customFonts, | ||||
|     ), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @@ -22,24 +31,36 @@ Future<ThemeData> createAppTheme( | ||||
|   Brightness brightness, { | ||||
|   Color? seedColorOverride, | ||||
|   bool? useMaterial3, | ||||
|   String? customFonts, | ||||
| }) async { | ||||
|   final prefs = await SharedPreferences.getInstance(); | ||||
|  | ||||
|   final seedColorString = prefs.getInt(kAppColorSchemeStoreKey); | ||||
|   final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo; | ||||
|   final seedColor = | ||||
|       seedColorString != null ? Color(seedColorString) : Colors.indigo; | ||||
|  | ||||
|   final colorScheme = ColorScheme.fromSeed( | ||||
|     seedColor: seedColorOverride ?? seedColor, | ||||
|     brightness: brightness, | ||||
|   ); | ||||
|  | ||||
|   final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||
|   final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); | ||||
|   final hasAppBarTransparent = | ||||
|       prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||
|   final useM3 = | ||||
|       useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); | ||||
|  | ||||
|   final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts)) | ||||
|           ?.split(',') | ||||
|           .map((ele) => ele.trim()) | ||||
|           .toList() ?? | ||||
|       ['Nunito']; | ||||
|  | ||||
|   return ThemeData( | ||||
|     useMaterial3: useM3, | ||||
|     colorScheme: colorScheme, | ||||
|     brightness: brightness, | ||||
|     fontFamily: inUseFonts.firstOrNull, | ||||
|     fontFamilyFallback: inUseFonts.sublist(1), | ||||
|     iconTheme: IconThemeData( | ||||
|       fill: 0, | ||||
|       weight: 400, | ||||
| @@ -52,8 +73,10 @@ Future<ThemeData> createAppTheme( | ||||
|     appBarTheme: AppBarTheme( | ||||
|       centerTitle: true, | ||||
|       elevation: hasAppBarTransparent ? 0 : null, | ||||
|       backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary, | ||||
|       foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary, | ||||
|       backgroundColor: | ||||
|           hasAppBarTransparent ? Colors.transparent : colorScheme.primary, | ||||
|       foregroundColor: | ||||
|           hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary, | ||||
|     ), | ||||
|     pageTransitionsTheme: PageTransitionsTheme( | ||||
|       builders: { | ||||
| @@ -67,3 +90,20 @@ Future<ThemeData> createAppTheme( | ||||
|     ), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| extension HexColor on Color { | ||||
|   /// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#". | ||||
|   static Color fromHex(String hexString) { | ||||
|     final buffer = StringBuffer(); | ||||
|     if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); | ||||
|     buffer.write(hexString.replaceFirst('#', '')); | ||||
|     return Color(int.parse(buffer.toString(), radix: 16)); | ||||
|   } | ||||
|  | ||||
|   /// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`). | ||||
|   String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}' | ||||
|       '${alpha.toRadixString(16).padLeft(2, '0')}' | ||||
|       '${red.toRadixString(16).padLeft(2, '0')}' | ||||
|       '${green.toRadixString(16).padLeft(2, '0')}' | ||||
|       '${blue.toRadixString(16).padLeft(2, '0')}'; | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ part 'account.freezed.dart'; | ||||
| part 'account.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnAccount with _$SnAccount { | ||||
| abstract class SnAccount with _$SnAccount { | ||||
|   const SnAccount._(); | ||||
|  | ||||
|   const factory SnAccount({ | ||||
| @@ -16,7 +16,6 @@ class SnAccount with _$SnAccount { | ||||
|     required List<SnAccountContact>? contacts, | ||||
|     @Default("") String avatar, | ||||
|     @Default("") String banner, | ||||
|     required String description, | ||||
|     required String name, | ||||
|     required String nick, | ||||
|     @Default({}) Map<String, dynamic> permNodes, | ||||
| @@ -35,7 +34,7 @@ class SnAccount with _$SnAccount { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAccountContact with _$SnAccountContact { | ||||
| abstract class SnAccountContact with _$SnAccountContact { | ||||
|   const factory SnAccountContact({ | ||||
|     required int accountId, | ||||
|     required String content, | ||||
| @@ -54,18 +53,24 @@ class SnAccountContact with _$SnAccountContact { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAccountProfile with _$SnAccountProfile { | ||||
| abstract class SnAccountProfile with _$SnAccountProfile { | ||||
|   const factory SnAccountProfile({ | ||||
|     required int id, | ||||
|     required int accountId, | ||||
|     required DateTime? birthday, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     required int experience, | ||||
|     required String firstName, | ||||
|     required String lastName, | ||||
|     required String description, | ||||
|     required String timeZone, | ||||
|     required String location, | ||||
|     required String pronouns, | ||||
|     required String gender, | ||||
|     @Default({}) Map<String, String> links, | ||||
|     required int experience, | ||||
|     required DateTime? lastSeenAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? birthday, | ||||
|     required int accountId, | ||||
|   }) = _SnAccountProfile; | ||||
|  | ||||
|   factory SnAccountProfile.fromJson(Map<String, Object?> json) => | ||||
| @@ -73,7 +78,7 @@ class SnAccountProfile with _$SnAccountProfile { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnRelationship with _$SnRelationship { | ||||
| abstract class SnRelationship with _$SnRelationship { | ||||
|   const factory SnRelationship({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -92,7 +97,7 @@ class SnRelationship with _$SnRelationship { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAccountBadge with _$SnAccountBadge { | ||||
| abstract class SnAccountBadge with _$SnAccountBadge { | ||||
|   const factory SnAccountBadge({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -100,6 +105,7 @@ class SnAccountBadge with _$SnAccountBadge { | ||||
|     required dynamic deletedAt, | ||||
|     required String type, | ||||
|     required int accountId, | ||||
|     @Default(false) bool isActive, | ||||
|     @Default({}) Map<String, dynamic> metadata, | ||||
|   }) = _SnAccountBadge; | ||||
|  | ||||
| @@ -108,7 +114,7 @@ class SnAccountBadge with _$SnAccountBadge { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAccountStatusInfo with _$SnAccountStatusInfo { | ||||
| abstract class SnAccountStatusInfo with _$SnAccountStatusInfo { | ||||
|   const factory SnAccountStatusInfo({ | ||||
|     required bool isDisturbable, | ||||
|     required bool isOnline, | ||||
| @@ -121,7 +127,7 @@ class SnAccountStatusInfo with _$SnAccountStatusInfo { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAbuseReport with _$SnAbuseReport { | ||||
| abstract class SnAbuseReport with _$SnAbuseReport { | ||||
|   const factory SnAbuseReport({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,8 +6,7 @@ part of 'account.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAccountImpl( | ||||
| _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -22,7 +21,6 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) => | ||||
|           .toList(), | ||||
|       avatar: json['avatar'] as String? ?? "", | ||||
|       banner: json['banner'] as String? ?? "", | ||||
|       description: json['description'] as String, | ||||
|       name: json['name'] as String, | ||||
|       nick: json['nick'] as String, | ||||
|       permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {}, | ||||
| @@ -43,7 +41,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) => | ||||
|       automatedId: (json['automated_id'] as num?)?.toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) => | ||||
| Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -53,7 +51,6 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) => | ||||
|       'contacts': instance.contacts?.map((e) => e.toJson()).toList(), | ||||
|       'avatar': instance.avatar, | ||||
|       'banner': instance.banner, | ||||
|       'description': instance.description, | ||||
|       'name': instance.name, | ||||
|       'nick': instance.nick, | ||||
|       'perm_nodes': instance.permNodes, | ||||
| @@ -67,9 +64,8 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) => | ||||
|       'automated_id': instance.automatedId, | ||||
|     }; | ||||
|  | ||||
| _$SnAccountContactImpl _$$SnAccountContactImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAccountContactImpl( | ||||
| _SnAccountContact _$SnAccountContactFromJson(Map<String, dynamic> json) => | ||||
|     _SnAccountContact( | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|       content: json['content'] as String, | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
| @@ -86,8 +82,7 @@ _$SnAccountContactImpl _$$SnAccountContactImplFromJson( | ||||
|           : DateTime.parse(json['verified_at'] as String), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAccountContactImplToJson( | ||||
|         _$SnAccountContactImpl instance) => | ||||
| Map<String, dynamic> _$SnAccountContactToJson(_SnAccountContact instance) => | ||||
|     <String, dynamic>{ | ||||
|       'account_id': instance.accountId, | ||||
|       'content': instance.content, | ||||
| @@ -101,44 +96,57 @@ Map<String, dynamic> _$$SnAccountContactImplToJson( | ||||
|       'verified_at': instance.verifiedAt?.toIso8601String(), | ||||
|     }; | ||||
|  | ||||
| _$SnAccountProfileImpl _$$SnAccountProfileImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAccountProfileImpl( | ||||
| _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) => | ||||
|     _SnAccountProfile( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|       birthday: json['birthday'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['birthday'] as String), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       experience: (json['experience'] as num).toInt(), | ||||
|       firstName: json['first_name'] as String, | ||||
|       lastName: json['last_name'] as String, | ||||
|       description: json['description'] as String, | ||||
|       timeZone: json['time_zone'] as String, | ||||
|       location: json['location'] as String, | ||||
|       pronouns: json['pronouns'] as String, | ||||
|       gender: json['gender'] as String, | ||||
|       links: (json['links'] as Map<String, dynamic>?)?.map( | ||||
|             (k, e) => MapEntry(k, e as String), | ||||
|           ) ?? | ||||
|           const {}, | ||||
|       experience: (json['experience'] as num).toInt(), | ||||
|       lastSeenAt: json['last_seen_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['last_seen_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       birthday: json['birthday'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['birthday'] as String), | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAccountProfileImplToJson( | ||||
|         _$SnAccountProfileImpl instance) => | ||||
| Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'account_id': instance.accountId, | ||||
|       'birthday': instance.birthday?.toIso8601String(), | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'experience': instance.experience, | ||||
|       'first_name': instance.firstName, | ||||
|       'last_name': instance.lastName, | ||||
|       'description': instance.description, | ||||
|       'time_zone': instance.timeZone, | ||||
|       'location': instance.location, | ||||
|       'pronouns': instance.pronouns, | ||||
|       'gender': instance.gender, | ||||
|       'links': instance.links, | ||||
|       'experience': instance.experience, | ||||
|       'last_seen_at': instance.lastSeenAt?.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'birthday': instance.birthday?.toIso8601String(), | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnRelationshipImpl _$$SnRelationshipImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnRelationshipImpl( | ||||
| _SnRelationship _$SnRelationshipFromJson(Map<String, dynamic> json) => | ||||
|     _SnRelationship( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -157,8 +165,7 @@ _$SnRelationshipImpl _$$SnRelationshipImplFromJson(Map<String, dynamic> json) => | ||||
|       permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {}, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnRelationshipImplToJson( | ||||
|         _$SnRelationshipImpl instance) => | ||||
| Map<String, dynamic> _$SnRelationshipToJson(_SnRelationship instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -172,19 +179,19 @@ Map<String, dynamic> _$$SnRelationshipImplToJson( | ||||
|       'perm_nodes': instance.permNodes, | ||||
|     }; | ||||
|  | ||||
| _$SnAccountBadgeImpl _$$SnAccountBadgeImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAccountBadgeImpl( | ||||
| _SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) => | ||||
|     _SnAccountBadge( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'], | ||||
|       type: json['type'] as String, | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|       isActive: json['is_active'] as bool? ?? false, | ||||
|       metadata: json['metadata'] as Map<String, dynamic>? ?? const {}, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAccountBadgeImplToJson( | ||||
|         _$SnAccountBadgeImpl instance) => | ||||
| Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -192,12 +199,12 @@ Map<String, dynamic> _$$SnAccountBadgeImplToJson( | ||||
|       'deleted_at': instance.deletedAt, | ||||
|       'type': instance.type, | ||||
|       'account_id': instance.accountId, | ||||
|       'is_active': instance.isActive, | ||||
|       'metadata': instance.metadata, | ||||
|     }; | ||||
|  | ||||
| _$SnAccountStatusInfoImpl _$$SnAccountStatusInfoImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAccountStatusInfoImpl( | ||||
| _SnAccountStatusInfo _$SnAccountStatusInfoFromJson(Map<String, dynamic> json) => | ||||
|     _SnAccountStatusInfo( | ||||
|       isDisturbable: json['is_disturbable'] as bool, | ||||
|       isOnline: json['is_online'] as bool, | ||||
|       lastSeenAt: json['last_seen_at'] == null | ||||
| @@ -206,8 +213,8 @@ _$SnAccountStatusInfoImpl _$$SnAccountStatusInfoImplFromJson( | ||||
|       status: json['status'], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAccountStatusInfoImplToJson( | ||||
|         _$SnAccountStatusInfoImpl instance) => | ||||
| Map<String, dynamic> _$SnAccountStatusInfoToJson( | ||||
|         _SnAccountStatusInfo instance) => | ||||
|     <String, dynamic>{ | ||||
|       'is_disturbable': instance.isDisturbable, | ||||
|       'is_online': instance.isOnline, | ||||
| @@ -215,8 +222,8 @@ Map<String, dynamic> _$$SnAccountStatusInfoImplToJson( | ||||
|       'status': instance.status, | ||||
|     }; | ||||
|  | ||||
| _$SnAbuseReportImpl _$$SnAbuseReportImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAbuseReportImpl( | ||||
| _SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) => | ||||
|     _SnAbuseReport( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -229,7 +236,7 @@ _$SnAbuseReportImpl _$$SnAbuseReportImplFromJson(Map<String, dynamic> json) => | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAbuseReportImplToJson(_$SnAbuseReportImpl instance) => | ||||
| Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -12,7 +12,7 @@ enum SnMediaType { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAttachment with _$SnAttachment { | ||||
| abstract class SnAttachment with _$SnAttachment { | ||||
|   const SnAttachment._(); | ||||
|  | ||||
|   const factory SnAttachment({ | ||||
| @@ -65,7 +65,7 @@ class SnAttachment with _$SnAttachment { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAttachmentFragment with _$SnAttachmentFragment { | ||||
| abstract class SnAttachmentFragment with _$SnAttachmentFragment { | ||||
|   const SnAttachmentFragment._(); | ||||
|  | ||||
|   const factory SnAttachmentFragment({ | ||||
| @@ -96,7 +96,7 @@ class SnAttachmentFragment with _$SnAttachmentFragment { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAttachmentPool with _$SnAttachmentPool { | ||||
| abstract class SnAttachmentPool with _$SnAttachmentPool { | ||||
|   const factory SnAttachmentPool({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -113,7 +113,7 @@ class SnAttachmentPool with _$SnAttachmentPool { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAttachmentDestination with _$SnAttachmentDestination { | ||||
| abstract class SnAttachmentDestination with _$SnAttachmentDestination { | ||||
|   const factory SnAttachmentDestination({ | ||||
|     @Default(0) int id, | ||||
|     required String type, | ||||
| @@ -126,7 +126,7 @@ class SnAttachmentDestination with _$SnAttachmentDestination { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAttachmentBoost with _$SnAttachmentBoost { | ||||
| abstract class SnAttachmentBoost with _$SnAttachmentBoost { | ||||
|   const factory SnAttachmentBoost({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -143,7 +143,7 @@ class SnAttachmentBoost with _$SnAttachmentBoost { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnSticker with _$SnSticker { | ||||
| abstract class SnSticker with _$SnSticker { | ||||
|   const factory SnSticker({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -162,7 +162,7 @@ class SnSticker with _$SnSticker { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnStickerPack with _$SnStickerPack { | ||||
| abstract class SnStickerPack with _$SnStickerPack { | ||||
|   const factory SnStickerPack({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -179,7 +179,7 @@ class SnStickerPack with _$SnStickerPack { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAttachmentBilling with _$SnAttachmentBilling { | ||||
| abstract class SnAttachmentBilling with _$SnAttachmentBilling { | ||||
|   const factory SnAttachmentBilling({ | ||||
|     required int currentBytes, | ||||
|     required int discountFileSize, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,8 +6,8 @@ part of 'attachment.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAttachmentImpl( | ||||
| _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) => | ||||
|     _SnAttachment( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -57,7 +57,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) => | ||||
|       metadata: json['metadata'] as Map<String, dynamic>? ?? const {}, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => | ||||
| Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -92,9 +92,9 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => | ||||
|       'metadata': instance.metadata, | ||||
|     }; | ||||
|  | ||||
| _$SnAttachmentFragmentImpl _$$SnAttachmentFragmentImplFromJson( | ||||
| _SnAttachmentFragment _$SnAttachmentFragmentFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAttachmentFragmentImpl( | ||||
|     _SnAttachmentFragment( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -119,8 +119,8 @@ _$SnAttachmentFragmentImpl _$$SnAttachmentFragmentImplFromJson( | ||||
|           const [], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentFragmentImplToJson( | ||||
|         _$SnAttachmentFragmentImpl instance) => | ||||
| Map<String, dynamic> _$SnAttachmentFragmentToJson( | ||||
|         _SnAttachmentFragment instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -138,9 +138,8 @@ Map<String, dynamic> _$$SnAttachmentFragmentImplToJson( | ||||
|       'file_chunks_missing': instance.fileChunksMissing, | ||||
|     }; | ||||
|  | ||||
| _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAttachmentPoolImpl( | ||||
| _SnAttachmentPool _$SnAttachmentPoolFromJson(Map<String, dynamic> json) => | ||||
|     _SnAttachmentPool( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -154,8 +153,7 @@ _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson( | ||||
|       accountId: (json['account_id'] as num?)?.toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentPoolImplToJson( | ||||
|         _$SnAttachmentPoolImpl instance) => | ||||
| Map<String, dynamic> _$SnAttachmentPoolToJson(_SnAttachmentPool instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -168,9 +166,9 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson( | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson( | ||||
| _SnAttachmentDestination _$SnAttachmentDestinationFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAttachmentDestinationImpl( | ||||
|     _SnAttachmentDestination( | ||||
|       id: (json['id'] as num?)?.toInt() ?? 0, | ||||
|       type: json['type'] as String, | ||||
|       label: json['label'] as String, | ||||
| @@ -178,8 +176,8 @@ _$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson( | ||||
|       isBoost: json['is_boost'] as bool, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentDestinationImplToJson( | ||||
|         _$SnAttachmentDestinationImpl instance) => | ||||
| Map<String, dynamic> _$SnAttachmentDestinationToJson( | ||||
|         _SnAttachmentDestination instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'type': instance.type, | ||||
| @@ -188,9 +186,8 @@ Map<String, dynamic> _$$SnAttachmentDestinationImplToJson( | ||||
|       'is_boost': instance.isBoost, | ||||
|     }; | ||||
|  | ||||
| _$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAttachmentBoostImpl( | ||||
| _SnAttachmentBoost _$SnAttachmentBoostFromJson(Map<String, dynamic> json) => | ||||
|     _SnAttachmentBoost( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -205,8 +202,7 @@ _$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson( | ||||
|       account: (json['account'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentBoostImplToJson( | ||||
|         _$SnAttachmentBoostImpl instance) => | ||||
| Map<String, dynamic> _$SnAttachmentBoostToJson(_SnAttachmentBoost instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -219,8 +215,7 @@ Map<String, dynamic> _$$SnAttachmentBoostImplToJson( | ||||
|       'account': instance.account, | ||||
|     }; | ||||
|  | ||||
| _$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnStickerImpl( | ||||
| _SnSticker _$SnStickerFromJson(Map<String, dynamic> json) => _SnSticker( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -237,7 +232,7 @@ _$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) => | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) => | ||||
| Map<String, dynamic> _$SnStickerToJson(_SnSticker instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -252,8 +247,8 @@ Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) => | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnStickerPackImpl( | ||||
| _SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) => | ||||
|     _SnStickerPack( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -269,7 +264,7 @@ _$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) => | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) => | ||||
| Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -282,16 +277,15 @@ Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) => | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnAttachmentBillingImpl _$$SnAttachmentBillingImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnAttachmentBillingImpl( | ||||
| _SnAttachmentBilling _$SnAttachmentBillingFromJson(Map<String, dynamic> json) => | ||||
|     _SnAttachmentBilling( | ||||
|       currentBytes: (json['current_bytes'] as num).toInt(), | ||||
|       discountFileSize: (json['discount_file_size'] as num).toInt(), | ||||
|       includedRatio: (json['included_ratio'] as num).toDouble(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentBillingImplToJson( | ||||
|         _$SnAttachmentBillingImpl instance) => | ||||
| Map<String, dynamic> _$SnAttachmentBillingToJson( | ||||
|         _SnAttachmentBilling instance) => | ||||
|     <String, dynamic>{ | ||||
|       'current_bytes': instance.currentBytes, | ||||
|       'discount_file_size': instance.discountFileSize, | ||||
|   | ||||
| @@ -4,7 +4,7 @@ part 'auth.freezed.dart'; | ||||
| part 'auth.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnAuthResult with _$SnAuthResult { | ||||
| abstract class SnAuthResult with _$SnAuthResult { | ||||
|   const factory SnAuthResult({ | ||||
|     required bool isFinished, | ||||
|     required SnAuthTicket? ticket, | ||||
| @@ -15,7 +15,7 @@ class SnAuthResult with _$SnAuthResult { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAuthTicket with _$SnAuthTicket { | ||||
| abstract class SnAuthTicket with _$SnAuthTicket { | ||||
|   const factory SnAuthTicket({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -41,7 +41,7 @@ class SnAuthTicket with _$SnAuthTicket { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnAuthFactor with _$SnAuthFactor { | ||||
| abstract class SnAuthFactor with _$SnAuthFactor { | ||||
|   const factory SnAuthFactor({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,22 +6,22 @@ part of 'auth.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnAuthResultImpl _$$SnAuthResultImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAuthResultImpl( | ||||
| _SnAuthResult _$SnAuthResultFromJson(Map<String, dynamic> json) => | ||||
|     _SnAuthResult( | ||||
|       isFinished: json['is_finished'] as bool, | ||||
|       ticket: json['ticket'] == null | ||||
|           ? null | ||||
|           : SnAuthTicket.fromJson(json['ticket'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAuthResultImplToJson(_$SnAuthResultImpl instance) => | ||||
| Map<String, dynamic> _$SnAuthResultToJson(_SnAuthResult instance) => | ||||
|     <String, dynamic>{ | ||||
|       'is_finished': instance.isFinished, | ||||
|       'ticket': instance.ticket?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _$SnAuthTicketImpl _$$SnAuthTicketImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAuthTicketImpl( | ||||
| _SnAuthTicket _$SnAuthTicketFromJson(Map<String, dynamic> json) => | ||||
|     _SnAuthTicket( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -52,7 +52,7 @@ _$SnAuthTicketImpl _$$SnAuthTicketImplFromJson(Map<String, dynamic> json) => | ||||
|           const [], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAuthTicketImplToJson(_$SnAuthTicketImpl instance) => | ||||
| Map<String, dynamic> _$SnAuthTicketToJson(_SnAuthTicket instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -73,8 +73,8 @@ Map<String, dynamic> _$$SnAuthTicketImplToJson(_$SnAuthTicketImpl instance) => | ||||
|       'factor_trail': instance.factorTrail, | ||||
|     }; | ||||
|  | ||||
| _$SnAuthFactorImpl _$$SnAuthFactorImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnAuthFactorImpl( | ||||
| _SnAuthFactor _$SnAuthFactorFromJson(Map<String, dynamic> json) => | ||||
|     _SnAuthFactor( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -86,7 +86,7 @@ _$SnAuthFactorImpl _$$SnAuthFactorImplFromJson(Map<String, dynamic> json) => | ||||
|       accountId: (json['account_id'] as num?)?.toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAuthFactorImplToJson(_$SnAuthFactorImpl instance) => | ||||
| Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -8,7 +8,7 @@ part 'chat.freezed.dart'; | ||||
| part 'chat.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnChannel with _$SnChannel { | ||||
| abstract class SnChannel with _$SnChannel { | ||||
|   const SnChannel._(); | ||||
|  | ||||
|   const factory SnChannel({ | ||||
| @@ -37,7 +37,7 @@ class SnChannel with _$SnChannel { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnChannelMember with _$SnChannelMember { | ||||
| abstract class SnChannelMember with _$SnChannelMember { | ||||
|   const SnChannelMember._(); | ||||
|  | ||||
|   const factory SnChannelMember({ | ||||
| @@ -61,7 +61,7 @@ class SnChannelMember with _$SnChannelMember { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnChatMessage with _$SnChatMessage { | ||||
| abstract class SnChatMessage with _$SnChatMessage { | ||||
|   const SnChatMessage._(); | ||||
|  | ||||
|   const factory SnChatMessage({ | ||||
| @@ -86,7 +86,7 @@ class SnChatMessage with _$SnChatMessage { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnChatMessagePreload with _$SnChatMessagePreload { | ||||
| abstract class SnChatMessagePreload with _$SnChatMessagePreload { | ||||
|   const SnChatMessagePreload._(); | ||||
|  | ||||
|   const factory SnChatMessagePreload({ | ||||
| @@ -99,7 +99,7 @@ class SnChatMessagePreload with _$SnChatMessagePreload { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnChatCall with _$SnChatCall { | ||||
| abstract class SnChatCall with _$SnChatCall { | ||||
|   const factory SnChatCall({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,8 +6,7 @@ part of 'chat.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnChannelImpl _$$SnChannelImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnChannelImpl( | ||||
| _SnChannel _$SnChannelFromJson(Map<String, dynamic> json) => _SnChannel( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -31,7 +30,7 @@ _$SnChannelImpl _$$SnChannelImplFromJson(Map<String, dynamic> json) => | ||||
|       isCommunity: json['is_community'] as bool, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnChannelImplToJson(_$SnChannelImpl instance) => | ||||
| Map<String, dynamic> _$SnChannelToJson(_SnChannel instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -50,9 +49,8 @@ Map<String, dynamic> _$$SnChannelImplToJson(_$SnChannelImpl instance) => | ||||
|       'is_community': instance.isCommunity, | ||||
|     }; | ||||
|  | ||||
| _$SnChannelMemberImpl _$$SnChannelMemberImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnChannelMemberImpl( | ||||
| _SnChannelMember _$SnChannelMemberFromJson(Map<String, dynamic> json) => | ||||
|     _SnChannelMember( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -74,8 +72,7 @@ _$SnChannelMemberImpl _$$SnChannelMemberImplFromJson( | ||||
|       events: json['events'], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnChannelMemberImplToJson( | ||||
|         _$SnChannelMemberImpl instance) => | ||||
| Map<String, dynamic> _$SnChannelMemberToJson(_SnChannelMember instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -92,8 +89,8 @@ Map<String, dynamic> _$$SnChannelMemberImplToJson( | ||||
|       'events': instance.events, | ||||
|     }; | ||||
|  | ||||
| _$SnChatMessageImpl _$$SnChatMessageImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnChatMessageImpl( | ||||
| _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) => | ||||
|     _SnChatMessage( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -115,7 +112,7 @@ _$SnChatMessageImpl _$$SnChatMessageImplFromJson(Map<String, dynamic> json) => | ||||
|               json['preload'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) => | ||||
| Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -133,9 +130,9 @@ Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) => | ||||
|       'preload': instance.preload?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _$SnChatMessagePreloadImpl _$$SnChatMessagePreloadImplFromJson( | ||||
| _SnChatMessagePreload _$SnChatMessagePreloadFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnChatMessagePreloadImpl( | ||||
|     _SnChatMessagePreload( | ||||
|       attachments: (json['attachments'] as List<dynamic>?) | ||||
|           ?.map((e) => e == null | ||||
|               ? null | ||||
| @@ -146,15 +143,14 @@ _$SnChatMessagePreloadImpl _$$SnChatMessagePreloadImplFromJson( | ||||
|           : SnChatMessage.fromJson(json['quote_event'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnChatMessagePreloadImplToJson( | ||||
|         _$SnChatMessagePreloadImpl instance) => | ||||
| Map<String, dynamic> _$SnChatMessagePreloadToJson( | ||||
|         _SnChatMessagePreload instance) => | ||||
|     <String, dynamic>{ | ||||
|       'attachments': instance.attachments?.map((e) => e?.toJson()).toList(), | ||||
|       'quote_event': instance.quoteEvent?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _$SnChatCallImpl _$$SnChatCallImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnChatCallImpl( | ||||
| _SnChatCall _$SnChatCallFromJson(Map<String, dynamic> json) => _SnChatCall( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -172,7 +168,7 @@ _$SnChatCallImpl _$$SnChatCallImplFromJson(Map<String, dynamic> json) => | ||||
|       participants: json['participants'] as List<dynamic>? ?? const [], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnChatCallImplToJson(_$SnChatCallImpl instance) => | ||||
| Map<String, dynamic> _$SnChatCallToJson(_SnChatCall instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -14,7 +14,7 @@ final List<String> kCheckInResultTierSymbols = [ | ||||
| ].map((e) => e.tr()).toList(); | ||||
|  | ||||
| @freezed | ||||
| class SnCheckInRecord with _$SnCheckInRecord { | ||||
| abstract class SnCheckInRecord with _$SnCheckInRecord { | ||||
|   const SnCheckInRecord._(); | ||||
|  | ||||
|   const factory SnCheckInRecord({ | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| // dart format width=80 | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| @@ -9,128 +10,81 @@ part of 'check_in.dart'; | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| final _privateConstructorUsedError = UnsupportedError( | ||||
|     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); | ||||
|  | ||||
| SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) { | ||||
|   return _SnCheckInRecord.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnCheckInRecord { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   int get resultTier => throw _privateConstructorUsedError; | ||||
|   int get resultExperience => throw _privateConstructorUsedError; | ||||
|   double get resultCoin => throw _privateConstructorUsedError; | ||||
|   List<int> get resultModifiers => throw _privateConstructorUsedError; | ||||
|   int get accountId => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnCheckInRecord to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|   int get id; | ||||
|   DateTime get createdAt; | ||||
|   DateTime get updatedAt; | ||||
|   DateTime? get deletedAt; | ||||
|   int get resultTier; | ||||
|   int get resultExperience; | ||||
|   double get resultCoin; | ||||
|   List<int> get resultModifiers; | ||||
|   int get accountId; | ||||
|  | ||||
|   /// Create a copy of SnCheckInRecord | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnCheckInRecordCopyWith<SnCheckInRecord> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnCheckInRecordCopyWith<$Res> { | ||||
|   factory $SnCheckInRecordCopyWith( | ||||
|           SnCheckInRecord value, $Res Function(SnCheckInRecord) then) = | ||||
|       _$SnCheckInRecordCopyWithImpl<$Res, SnCheckInRecord>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       int resultTier, | ||||
|       int resultExperience, | ||||
|       double resultCoin, | ||||
|       List<int> resultModifiers, | ||||
|       int accountId}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord> | ||||
|     implements $SnCheckInRecordCopyWith<$Res> { | ||||
|   _$SnCheckInRecordCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnCheckInRecord | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnCheckInRecordCopyWith<SnCheckInRecord> get copyWith => | ||||
|       _$SnCheckInRecordCopyWithImpl<SnCheckInRecord>( | ||||
|           this as SnCheckInRecord, _$identity); | ||||
|  | ||||
|   /// Serializes this SnCheckInRecord to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? resultTier = null, | ||||
|     Object? resultExperience = null, | ||||
|     Object? resultCoin = null, | ||||
|     Object? resultModifiers = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       resultTier: null == resultTier | ||||
|           ? _value.resultTier | ||||
|           : resultTier // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultExperience: null == resultExperience | ||||
|           ? _value.resultExperience | ||||
|           : resultExperience // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultCoin: null == resultCoin | ||||
|           ? _value.resultCoin | ||||
|           : resultCoin // ignore: cast_nullable_to_non_nullable | ||||
|               as double, | ||||
|       resultModifiers: null == resultModifiers | ||||
|           ? _value.resultModifiers | ||||
|           : resultModifiers // ignore: cast_nullable_to_non_nullable | ||||
|               as List<int>, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     ) as $Val); | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is SnCheckInRecord && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.resultTier, resultTier) || | ||||
|                 other.resultTier == resultTier) && | ||||
|             (identical(other.resultExperience, resultExperience) || | ||||
|                 other.resultExperience == resultExperience) && | ||||
|             (identical(other.resultCoin, resultCoin) || | ||||
|                 other.resultCoin == resultCoin) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.resultModifiers, resultModifiers) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       id, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       deletedAt, | ||||
|       resultTier, | ||||
|       resultExperience, | ||||
|       resultCoin, | ||||
|       const DeepCollectionEquality().hash(resultModifiers), | ||||
|       accountId); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnCheckInRecordImplCopyWith<$Res> | ||||
|     implements $SnCheckInRecordCopyWith<$Res> { | ||||
|   factory _$$SnCheckInRecordImplCopyWith(_$SnCheckInRecordImpl value, | ||||
|           $Res Function(_$SnCheckInRecordImpl) then) = | ||||
|       __$$SnCheckInRecordImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
| abstract mixin class $SnCheckInRecordCopyWith<$Res> { | ||||
|   factory $SnCheckInRecordCopyWith( | ||||
|           SnCheckInRecord value, $Res Function(SnCheckInRecord) _then) = | ||||
|       _$SnCheckInRecordCopyWithImpl; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
| @@ -145,12 +99,12 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res> | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnCheckInRecordImplCopyWithImpl<$Res> | ||||
|     extends _$SnCheckInRecordCopyWithImpl<$Res, _$SnCheckInRecordImpl> | ||||
|     implements _$$SnCheckInRecordImplCopyWith<$Res> { | ||||
|   __$$SnCheckInRecordImplCopyWithImpl( | ||||
|       _$SnCheckInRecordImpl _value, $Res Function(_$SnCheckInRecordImpl) _then) | ||||
|       : super(_value, _then); | ||||
| class _$SnCheckInRecordCopyWithImpl<$Res> | ||||
|     implements $SnCheckInRecordCopyWith<$Res> { | ||||
|   _$SnCheckInRecordCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnCheckInRecord _self; | ||||
|   final $Res Function(SnCheckInRecord) _then; | ||||
|  | ||||
|   /// Create a copy of SnCheckInRecord | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -167,41 +121,41 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res> | ||||
|     Object? resultModifiers = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_$SnCheckInRecordImpl( | ||||
|     return _then(_self.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           ? _self.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           ? _self.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           ? _self.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       resultTier: null == resultTier | ||||
|           ? _value.resultTier | ||||
|           ? _self.resultTier | ||||
|           : resultTier // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultExperience: null == resultExperience | ||||
|           ? _value.resultExperience | ||||
|           ? _self.resultExperience | ||||
|           : resultExperience // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultCoin: null == resultCoin | ||||
|           ? _value.resultCoin | ||||
|           ? _self.resultCoin | ||||
|           : resultCoin // ignore: cast_nullable_to_non_nullable | ||||
|               as double, | ||||
|       resultModifiers: null == resultModifiers | ||||
|           ? _value._resultModifiers | ||||
|           ? _self.resultModifiers | ||||
|           : resultModifiers // ignore: cast_nullable_to_non_nullable | ||||
|               as List<int>, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           ? _self.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     )); | ||||
| @@ -210,8 +164,8 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res> | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|   const _$SnCheckInRecordImpl( | ||||
| class _SnCheckInRecord extends SnCheckInRecord { | ||||
|   const _SnCheckInRecord( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
| @@ -223,9 +177,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|       required this.accountId}) | ||||
|       : _resultModifiers = resultModifiers, | ||||
|         super._(); | ||||
|  | ||||
|   factory _$SnCheckInRecordImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnCheckInRecordImplFromJson(json); | ||||
|   factory _SnCheckInRecord.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnCheckInRecordFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
| @@ -252,16 +205,26 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|   @override | ||||
|   final int accountId; | ||||
|  | ||||
|   /// Create a copy of SnCheckInRecord | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)'; | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$SnCheckInRecordCopyWith<_SnCheckInRecord> get copyWith => | ||||
|       __$SnCheckInRecordCopyWithImpl<_SnCheckInRecord>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$SnCheckInRecordToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnCheckInRecordImpl && | ||||
|             other is _SnCheckInRecord && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
| @@ -295,62 +258,94 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|       const DeepCollectionEquality().hash(_resultModifiers), | ||||
|       accountId); | ||||
|  | ||||
|   /// Create a copy of SnCheckInRecord | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnCheckInRecordImplCopyWith<_$SnCheckInRecordImpl> get copyWith => | ||||
|       __$$SnCheckInRecordImplCopyWithImpl<_$SnCheckInRecordImpl>( | ||||
|           this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnCheckInRecordImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   String toString() { | ||||
|     return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnCheckInRecord extends SnCheckInRecord { | ||||
|   const factory _SnCheckInRecord( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final int resultTier, | ||||
|       required final int resultExperience, | ||||
|       required final double resultCoin, | ||||
|       required final List<int> resultModifiers, | ||||
|       required final int accountId}) = _$SnCheckInRecordImpl; | ||||
|   const _SnCheckInRecord._() : super._(); | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnCheckInRecordCopyWith<$Res> | ||||
|     implements $SnCheckInRecordCopyWith<$Res> { | ||||
|   factory _$SnCheckInRecordCopyWith( | ||||
|           _SnCheckInRecord value, $Res Function(_SnCheckInRecord) _then) = | ||||
|       __$SnCheckInRecordCopyWithImpl; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       int resultTier, | ||||
|       int resultExperience, | ||||
|       double resultCoin, | ||||
|       List<int> resultModifiers, | ||||
|       int accountId}); | ||||
| } | ||||
|  | ||||
|   factory _SnCheckInRecord.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnCheckInRecordImpl.fromJson; | ||||
| /// @nodoc | ||||
| class __$SnCheckInRecordCopyWithImpl<$Res> | ||||
|     implements _$SnCheckInRecordCopyWith<$Res> { | ||||
|   __$SnCheckInRecordCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   @override | ||||
|   DateTime? get deletedAt; | ||||
|   @override | ||||
|   int get resultTier; | ||||
|   @override | ||||
|   int get resultExperience; | ||||
|   @override | ||||
|   double get resultCoin; | ||||
|   @override | ||||
|   List<int> get resultModifiers; | ||||
|   @override | ||||
|   int get accountId; | ||||
|   final _SnCheckInRecord _self; | ||||
|   final $Res Function(_SnCheckInRecord) _then; | ||||
|  | ||||
|   /// Create a copy of SnCheckInRecord | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnCheckInRecordImplCopyWith<_$SnCheckInRecordImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? resultTier = null, | ||||
|     Object? resultExperience = null, | ||||
|     Object? resultCoin = null, | ||||
|     Object? resultModifiers = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_SnCheckInRecord( | ||||
|       id: null == id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       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, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _self.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       resultTier: null == resultTier | ||||
|           ? _self.resultTier | ||||
|           : resultTier // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultExperience: null == resultExperience | ||||
|           ? _self.resultExperience | ||||
|           : resultExperience // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultCoin: null == resultCoin | ||||
|           ? _self.resultCoin | ||||
|           : resultCoin // ignore: cast_nullable_to_non_nullable | ||||
|               as double, | ||||
|       resultModifiers: null == resultModifiers | ||||
|           ? _self._resultModifiers | ||||
|           : resultModifiers // ignore: cast_nullable_to_non_nullable | ||||
|               as List<int>, | ||||
|       accountId: null == accountId | ||||
|           ? _self.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
|   | ||||
| @@ -6,9 +6,8 @@ part of 'check_in.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$SnCheckInRecordImpl( | ||||
| _SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) => | ||||
|     _SnCheckInRecord( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -24,8 +23,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson( | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnCheckInRecordImplToJson( | ||||
|         _$SnCheckInRecordImpl instance) => | ||||
| Map<String, dynamic> _$SnCheckInRecordToJson(_SnCheckInRecord instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
							
								
								
									
										18
									
								
								lib/types/keypair.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								lib/types/keypair.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
|  | ||||
| part 'keypair.freezed.dart'; | ||||
| part 'keypair.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| abstract class SnKeyPair with _$SnKeyPair { | ||||
|   const factory SnKeyPair({ | ||||
|     required String id, | ||||
|     required int accountId, | ||||
|     required String publicKey, | ||||
|     bool? isActive, | ||||
|     String? privateKey, | ||||
|   }) = _SnKeyPair; | ||||
|  | ||||
|   factory SnKeyPair.fromJson(Map<String, Object?> json) => | ||||
|       _$SnKeyPairFromJson(json); | ||||
| } | ||||
							
								
								
									
										241
									
								
								lib/types/keypair.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								lib/types/keypair.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| // dart format width=80 | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark | ||||
|  | ||||
| part of 'keypair.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnKeyPair { | ||||
|   String get id; | ||||
|   int get accountId; | ||||
|   String get publicKey; | ||||
|   bool? get isActive; | ||||
|   String? get privateKey; | ||||
|  | ||||
|   /// Create a copy of SnKeyPair | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnKeyPairCopyWith<SnKeyPair> get copyWith => | ||||
|       _$SnKeyPairCopyWithImpl<SnKeyPair>(this as SnKeyPair, _$identity); | ||||
|  | ||||
|   /// Serializes this SnKeyPair to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is SnKeyPair && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId) && | ||||
|             (identical(other.publicKey, publicKey) || | ||||
|                 other.publicKey == publicKey) && | ||||
|             (identical(other.isActive, isActive) || | ||||
|                 other.isActive == isActive) && | ||||
|             (identical(other.privateKey, privateKey) || | ||||
|                 other.privateKey == privateKey)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => | ||||
|       Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnKeyPairCopyWith<$Res> { | ||||
|   factory $SnKeyPairCopyWith(SnKeyPair value, $Res Function(SnKeyPair) _then) = | ||||
|       _$SnKeyPairCopyWithImpl; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {String id, | ||||
|       int accountId, | ||||
|       String publicKey, | ||||
|       bool? isActive, | ||||
|       String? privateKey}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnKeyPairCopyWithImpl<$Res> implements $SnKeyPairCopyWith<$Res> { | ||||
|   _$SnKeyPairCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnKeyPair _self; | ||||
|   final $Res Function(SnKeyPair) _then; | ||||
|  | ||||
|   /// Create a copy of SnKeyPair | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? accountId = null, | ||||
|     Object? publicKey = null, | ||||
|     Object? isActive = freezed, | ||||
|     Object? privateKey = freezed, | ||||
|   }) { | ||||
|     return _then(_self.copyWith( | ||||
|       id: null == id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       accountId: null == accountId | ||||
|           ? _self.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       publicKey: null == publicKey | ||||
|           ? _self.publicKey | ||||
|           : publicKey // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       isActive: freezed == isActive | ||||
|           ? _self.isActive | ||||
|           : isActive // ignore: cast_nullable_to_non_nullable | ||||
|               as bool?, | ||||
|       privateKey: freezed == privateKey | ||||
|           ? _self.privateKey | ||||
|           : privateKey // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _SnKeyPair implements SnKeyPair { | ||||
|   const _SnKeyPair( | ||||
|       {required this.id, | ||||
|       required this.accountId, | ||||
|       required this.publicKey, | ||||
|       this.isActive, | ||||
|       this.privateKey}); | ||||
|   factory _SnKeyPair.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnKeyPairFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final String id; | ||||
|   @override | ||||
|   final int accountId; | ||||
|   @override | ||||
|   final String publicKey; | ||||
|   @override | ||||
|   final bool? isActive; | ||||
|   @override | ||||
|   final String? privateKey; | ||||
|  | ||||
|   /// Create a copy of SnKeyPair | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$SnKeyPairCopyWith<_SnKeyPair> get copyWith => | ||||
|       __$SnKeyPairCopyWithImpl<_SnKeyPair>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$SnKeyPairToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _SnKeyPair && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId) && | ||||
|             (identical(other.publicKey, publicKey) || | ||||
|                 other.publicKey == publicKey) && | ||||
|             (identical(other.isActive, isActive) || | ||||
|                 other.isActive == isActive) && | ||||
|             (identical(other.privateKey, privateKey) || | ||||
|                 other.privateKey == privateKey)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => | ||||
|       Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnKeyPairCopyWith<$Res> | ||||
|     implements $SnKeyPairCopyWith<$Res> { | ||||
|   factory _$SnKeyPairCopyWith( | ||||
|           _SnKeyPair value, $Res Function(_SnKeyPair) _then) = | ||||
|       __$SnKeyPairCopyWithImpl; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {String id, | ||||
|       int accountId, | ||||
|       String publicKey, | ||||
|       bool? isActive, | ||||
|       String? privateKey}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$SnKeyPairCopyWithImpl<$Res> implements _$SnKeyPairCopyWith<$Res> { | ||||
|   __$SnKeyPairCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnKeyPair _self; | ||||
|   final $Res Function(_SnKeyPair) _then; | ||||
|  | ||||
|   /// Create a copy of SnKeyPair | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? accountId = null, | ||||
|     Object? publicKey = null, | ||||
|     Object? isActive = freezed, | ||||
|     Object? privateKey = freezed, | ||||
|   }) { | ||||
|     return _then(_SnKeyPair( | ||||
|       id: null == id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       accountId: null == accountId | ||||
|           ? _self.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       publicKey: null == publicKey | ||||
|           ? _self.publicKey | ||||
|           : publicKey // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       isActive: freezed == isActive | ||||
|           ? _self.isActive | ||||
|           : isActive // ignore: cast_nullable_to_non_nullable | ||||
|               as bool?, | ||||
|       privateKey: freezed == privateKey | ||||
|           ? _self.privateKey | ||||
|           : privateKey // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										24
									
								
								lib/types/keypair.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/types/keypair.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'keypair.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _SnKeyPair _$SnKeyPairFromJson(Map<String, dynamic> json) => _SnKeyPair( | ||||
|       id: json['id'] as String, | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|       publicKey: json['public_key'] as String, | ||||
|       isActive: json['is_active'] as bool?, | ||||
|       privateKey: json['private_key'] as String?, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$SnKeyPairToJson(_SnKeyPair instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'account_id': instance.accountId, | ||||
|       'public_key': instance.publicKey, | ||||
|       'is_active': instance.isActive, | ||||
|       'private_key': instance.privateKey, | ||||
|     }; | ||||
| @@ -4,7 +4,7 @@ part 'link.g.dart'; | ||||
| part 'link.freezed.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnLinkMeta with _$SnLinkMeta { | ||||
| abstract class SnLinkMeta with _$SnLinkMeta { | ||||
|   const SnLinkMeta._(); | ||||
|  | ||||
|   const factory SnLinkMeta({ | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| // dart format width=80 | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| @@ -9,332 +10,41 @@ part of 'link.dart'; | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| final _privateConstructorUsedError = UnsupportedError( | ||||
|     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); | ||||
|  | ||||
| SnLinkMeta _$SnLinkMetaFromJson(Map<String, dynamic> json) { | ||||
|   return _SnLinkMeta.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnLinkMeta { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get entryId => throw _privateConstructorUsedError; | ||||
|   String? get icon => throw _privateConstructorUsedError; | ||||
|   String get url => throw _privateConstructorUsedError; | ||||
|   String? get title => throw _privateConstructorUsedError; | ||||
|   String? get image => throw _privateConstructorUsedError; | ||||
|   String? get video => throw _privateConstructorUsedError; | ||||
|   String? get audio => throw _privateConstructorUsedError; | ||||
|   String? get description => throw _privateConstructorUsedError; | ||||
|   String? get siteName => throw _privateConstructorUsedError; | ||||
|   String? get type => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnLinkMeta to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|   int get id; | ||||
|   DateTime get createdAt; | ||||
|   DateTime get updatedAt; | ||||
|   DateTime? get deletedAt; | ||||
|   String get entryId; | ||||
|   String? get icon; | ||||
|   String get url; | ||||
|   String? get title; | ||||
|   String? get image; | ||||
|   String? get video; | ||||
|   String? get audio; | ||||
|   String? get description; | ||||
|   String? get siteName; | ||||
|   String? get type; | ||||
|  | ||||
|   /// Create a copy of SnLinkMeta | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnLinkMetaCopyWith<SnLinkMeta> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|       _$SnLinkMetaCopyWithImpl<SnLinkMeta>(this as SnLinkMeta, _$identity); | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnLinkMetaCopyWith<$Res> { | ||||
|   factory $SnLinkMetaCopyWith( | ||||
|           SnLinkMeta value, $Res Function(SnLinkMeta) then) = | ||||
|       _$SnLinkMetaCopyWithImpl<$Res, SnLinkMeta>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String entryId, | ||||
|       String? icon, | ||||
|       String url, | ||||
|       String? title, | ||||
|       String? image, | ||||
|       String? video, | ||||
|       String? audio, | ||||
|       String? description, | ||||
|       String? siteName, | ||||
|       String? type}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnLinkMetaCopyWithImpl<$Res, $Val extends SnLinkMeta> | ||||
|     implements $SnLinkMetaCopyWith<$Res> { | ||||
|   _$SnLinkMetaCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnLinkMeta | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? entryId = null, | ||||
|     Object? icon = freezed, | ||||
|     Object? url = null, | ||||
|     Object? title = freezed, | ||||
|     Object? image = freezed, | ||||
|     Object? video = freezed, | ||||
|     Object? audio = freezed, | ||||
|     Object? description = freezed, | ||||
|     Object? siteName = freezed, | ||||
|     Object? type = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       entryId: null == entryId | ||||
|           ? _value.entryId | ||||
|           : entryId // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       icon: freezed == icon | ||||
|           ? _value.icon | ||||
|           : icon // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       url: null == url | ||||
|           ? _value.url | ||||
|           : url // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: freezed == title | ||||
|           ? _value.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       image: freezed == image | ||||
|           ? _value.image | ||||
|           : image // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       video: freezed == video | ||||
|           ? _value.video | ||||
|           : video // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       audio: freezed == audio | ||||
|           ? _value.audio | ||||
|           : audio // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       description: freezed == description | ||||
|           ? _value.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       siteName: freezed == siteName | ||||
|           ? _value.siteName | ||||
|           : siteName // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       type: freezed == type | ||||
|           ? _value.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     ) as $Val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnLinkMetaImplCopyWith<$Res> | ||||
|     implements $SnLinkMetaCopyWith<$Res> { | ||||
|   factory _$$SnLinkMetaImplCopyWith( | ||||
|           _$SnLinkMetaImpl value, $Res Function(_$SnLinkMetaImpl) then) = | ||||
|       __$$SnLinkMetaImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String entryId, | ||||
|       String? icon, | ||||
|       String url, | ||||
|       String? title, | ||||
|       String? image, | ||||
|       String? video, | ||||
|       String? audio, | ||||
|       String? description, | ||||
|       String? siteName, | ||||
|       String? type}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnLinkMetaImplCopyWithImpl<$Res> | ||||
|     extends _$SnLinkMetaCopyWithImpl<$Res, _$SnLinkMetaImpl> | ||||
|     implements _$$SnLinkMetaImplCopyWith<$Res> { | ||||
|   __$$SnLinkMetaImplCopyWithImpl( | ||||
|       _$SnLinkMetaImpl _value, $Res Function(_$SnLinkMetaImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnLinkMeta | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? entryId = null, | ||||
|     Object? icon = freezed, | ||||
|     Object? url = null, | ||||
|     Object? title = freezed, | ||||
|     Object? image = freezed, | ||||
|     Object? video = freezed, | ||||
|     Object? audio = freezed, | ||||
|     Object? description = freezed, | ||||
|     Object? siteName = freezed, | ||||
|     Object? type = freezed, | ||||
|   }) { | ||||
|     return _then(_$SnLinkMetaImpl( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       entryId: null == entryId | ||||
|           ? _value.entryId | ||||
|           : entryId // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       icon: freezed == icon | ||||
|           ? _value.icon | ||||
|           : icon // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       url: null == url | ||||
|           ? _value.url | ||||
|           : url // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: freezed == title | ||||
|           ? _value.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       image: freezed == image | ||||
|           ? _value.image | ||||
|           : image // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       video: freezed == video | ||||
|           ? _value.video | ||||
|           : video // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       audio: freezed == audio | ||||
|           ? _value.audio | ||||
|           : audio // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       description: freezed == description | ||||
|           ? _value.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       siteName: freezed == siteName | ||||
|           ? _value.siteName | ||||
|           : siteName // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       type: freezed == type | ||||
|           ? _value.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnLinkMetaImpl extends _SnLinkMeta { | ||||
|   const _$SnLinkMetaImpl( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.entryId, | ||||
|       required this.icon, | ||||
|       required this.url, | ||||
|       required this.title, | ||||
|       required this.image, | ||||
|       required this.video, | ||||
|       required this.audio, | ||||
|       required this.description, | ||||
|       required this.siteName, | ||||
|       required this.type}) | ||||
|       : super._(); | ||||
|  | ||||
|   factory _$SnLinkMetaImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnLinkMetaImplFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
|   @override | ||||
|   final DateTime createdAt; | ||||
|   @override | ||||
|   final DateTime updatedAt; | ||||
|   @override | ||||
|   final DateTime? deletedAt; | ||||
|   @override | ||||
|   final String entryId; | ||||
|   @override | ||||
|   final String? icon; | ||||
|   @override | ||||
|   final String url; | ||||
|   @override | ||||
|   final String? title; | ||||
|   @override | ||||
|   final String? image; | ||||
|   @override | ||||
|   final String? video; | ||||
|   @override | ||||
|   final String? audio; | ||||
|   @override | ||||
|   final String? description; | ||||
|   @override | ||||
|   final String? siteName; | ||||
|   @override | ||||
|   final String? type; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)'; | ||||
|   } | ||||
|   /// Serializes this SnLinkMeta to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnLinkMetaImpl && | ||||
|             other is SnLinkMeta && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
| @@ -375,76 +85,351 @@ class _$SnLinkMetaImpl extends _SnLinkMeta { | ||||
|       siteName, | ||||
|       type); | ||||
|  | ||||
|   /// Create a copy of SnLinkMeta | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith => | ||||
|       __$$SnLinkMetaImplCopyWithImpl<_$SnLinkMetaImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnLinkMetaImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   String toString() { | ||||
|     return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnLinkMeta extends SnLinkMeta { | ||||
|   const factory _SnLinkMeta( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final String entryId, | ||||
|       required final String? icon, | ||||
|       required final String url, | ||||
|       required final String? title, | ||||
|       required final String? image, | ||||
|       required final String? video, | ||||
|       required final String? audio, | ||||
|       required final String? description, | ||||
|       required final String? siteName, | ||||
|       required final String? type}) = _$SnLinkMetaImpl; | ||||
|   const _SnLinkMeta._() : super._(); | ||||
| /// @nodoc | ||||
| abstract mixin class $SnLinkMetaCopyWith<$Res> { | ||||
|   factory $SnLinkMetaCopyWith( | ||||
|           SnLinkMeta value, $Res Function(SnLinkMeta) _then) = | ||||
|       _$SnLinkMetaCopyWithImpl; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String entryId, | ||||
|       String? icon, | ||||
|       String url, | ||||
|       String? title, | ||||
|       String? image, | ||||
|       String? video, | ||||
|       String? audio, | ||||
|       String? description, | ||||
|       String? siteName, | ||||
|       String? type}); | ||||
| } | ||||
|  | ||||
|   factory _SnLinkMeta.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnLinkMetaImpl.fromJson; | ||||
| /// @nodoc | ||||
| class _$SnLinkMetaCopyWithImpl<$Res> implements $SnLinkMetaCopyWith<$Res> { | ||||
|   _$SnLinkMetaCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnLinkMeta _self; | ||||
|   final $Res Function(SnLinkMeta) _then; | ||||
|  | ||||
|   /// Create a copy of SnLinkMeta | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? entryId = null, | ||||
|     Object? icon = freezed, | ||||
|     Object? url = null, | ||||
|     Object? title = freezed, | ||||
|     Object? image = freezed, | ||||
|     Object? video = freezed, | ||||
|     Object? audio = freezed, | ||||
|     Object? description = freezed, | ||||
|     Object? siteName = freezed, | ||||
|     Object? type = freezed, | ||||
|   }) { | ||||
|     return _then(_self.copyWith( | ||||
|       id: null == id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       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, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _self.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       entryId: null == entryId | ||||
|           ? _self.entryId | ||||
|           : entryId // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       icon: freezed == icon | ||||
|           ? _self.icon | ||||
|           : icon // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       url: null == url | ||||
|           ? _self.url | ||||
|           : url // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: freezed == title | ||||
|           ? _self.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       image: freezed == image | ||||
|           ? _self.image | ||||
|           : image // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       video: freezed == video | ||||
|           ? _self.video | ||||
|           : video // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       audio: freezed == audio | ||||
|           ? _self.audio | ||||
|           : audio // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       description: freezed == description | ||||
|           ? _self.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       siteName: freezed == siteName | ||||
|           ? _self.siteName | ||||
|           : siteName // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       type: freezed == type | ||||
|           ? _self.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _SnLinkMeta extends SnLinkMeta { | ||||
|   const _SnLinkMeta( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.entryId, | ||||
|       required this.icon, | ||||
|       required this.url, | ||||
|       required this.title, | ||||
|       required this.image, | ||||
|       required this.video, | ||||
|       required this.audio, | ||||
|       required this.description, | ||||
|       required this.siteName, | ||||
|       required this.type}) | ||||
|       : super._(); | ||||
|   factory _SnLinkMeta.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnLinkMetaFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   final int id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   final DateTime createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   final DateTime updatedAt; | ||||
|   @override | ||||
|   DateTime? get deletedAt; | ||||
|   final DateTime? deletedAt; | ||||
|   @override | ||||
|   String get entryId; | ||||
|   final String entryId; | ||||
|   @override | ||||
|   String? get icon; | ||||
|   final String? icon; | ||||
|   @override | ||||
|   String get url; | ||||
|   final String url; | ||||
|   @override | ||||
|   String? get title; | ||||
|   final String? title; | ||||
|   @override | ||||
|   String? get image; | ||||
|   final String? image; | ||||
|   @override | ||||
|   String? get video; | ||||
|   final String? video; | ||||
|   @override | ||||
|   String? get audio; | ||||
|   final String? audio; | ||||
|   @override | ||||
|   String? get description; | ||||
|   final String? description; | ||||
|   @override | ||||
|   String? get siteName; | ||||
|   final String? siteName; | ||||
|   @override | ||||
|   String? get type; | ||||
|   final String? type; | ||||
|  | ||||
|   /// Create a copy of SnLinkMeta | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$SnLinkMetaCopyWith<_SnLinkMeta> get copyWith => | ||||
|       __$SnLinkMetaCopyWithImpl<_SnLinkMeta>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$SnLinkMetaToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _SnLinkMeta && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.entryId, entryId) || other.entryId == entryId) && | ||||
|             (identical(other.icon, icon) || other.icon == icon) && | ||||
|             (identical(other.url, url) || other.url == url) && | ||||
|             (identical(other.title, title) || other.title == title) && | ||||
|             (identical(other.image, image) || other.image == image) && | ||||
|             (identical(other.video, video) || other.video == video) && | ||||
|             (identical(other.audio, audio) || other.audio == audio) && | ||||
|             (identical(other.description, description) || | ||||
|                 other.description == description) && | ||||
|             (identical(other.siteName, siteName) || | ||||
|                 other.siteName == siteName) && | ||||
|             (identical(other.type, type) || other.type == type)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       id, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       deletedAt, | ||||
|       entryId, | ||||
|       icon, | ||||
|       url, | ||||
|       title, | ||||
|       image, | ||||
|       video, | ||||
|       audio, | ||||
|       description, | ||||
|       siteName, | ||||
|       type); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnLinkMetaCopyWith<$Res> | ||||
|     implements $SnLinkMetaCopyWith<$Res> { | ||||
|   factory _$SnLinkMetaCopyWith( | ||||
|           _SnLinkMeta value, $Res Function(_SnLinkMeta) _then) = | ||||
|       __$SnLinkMetaCopyWithImpl; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String entryId, | ||||
|       String? icon, | ||||
|       String url, | ||||
|       String? title, | ||||
|       String? image, | ||||
|       String? video, | ||||
|       String? audio, | ||||
|       String? description, | ||||
|       String? siteName, | ||||
|       String? type}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$SnLinkMetaCopyWithImpl<$Res> implements _$SnLinkMetaCopyWith<$Res> { | ||||
|   __$SnLinkMetaCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnLinkMeta _self; | ||||
|   final $Res Function(_SnLinkMeta) _then; | ||||
|  | ||||
|   /// Create a copy of SnLinkMeta | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? entryId = null, | ||||
|     Object? icon = freezed, | ||||
|     Object? url = null, | ||||
|     Object? title = freezed, | ||||
|     Object? image = freezed, | ||||
|     Object? video = freezed, | ||||
|     Object? audio = freezed, | ||||
|     Object? description = freezed, | ||||
|     Object? siteName = freezed, | ||||
|     Object? type = freezed, | ||||
|   }) { | ||||
|     return _then(_SnLinkMeta( | ||||
|       id: null == id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       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, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _self.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       entryId: null == entryId | ||||
|           ? _self.entryId | ||||
|           : entryId // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       icon: freezed == icon | ||||
|           ? _self.icon | ||||
|           : icon // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       url: null == url | ||||
|           ? _self.url | ||||
|           : url // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: freezed == title | ||||
|           ? _self.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       image: freezed == image | ||||
|           ? _self.image | ||||
|           : image // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       video: freezed == video | ||||
|           ? _self.video | ||||
|           : video // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       audio: freezed == audio | ||||
|           ? _self.audio | ||||
|           : audio // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       description: freezed == description | ||||
|           ? _self.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       siteName: freezed == siteName | ||||
|           ? _self.siteName | ||||
|           : siteName // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       type: freezed == type | ||||
|           ? _self.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
|   | ||||
| @@ -6,8 +6,7 @@ part of 'link.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnLinkMetaImpl _$$SnLinkMetaImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnLinkMetaImpl( | ||||
| _SnLinkMeta _$SnLinkMetaFromJson(Map<String, dynamic> json) => _SnLinkMeta( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -26,7 +25,7 @@ _$SnLinkMetaImpl _$$SnLinkMetaImplFromJson(Map<String, dynamic> json) => | ||||
|       type: json['type'] as String?, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnLinkMetaImplToJson(_$SnLinkMetaImpl instance) => | ||||
| Map<String, dynamic> _$SnLinkMetaToJson(_SnLinkMeta instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -4,7 +4,7 @@ part 'news.freezed.dart'; | ||||
| part 'news.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnNewsSource with _$SnNewsSource { | ||||
| abstract class SnNewsSource with _$SnNewsSource { | ||||
|   const factory SnNewsSource({ | ||||
|     required String id, | ||||
|     required String label, | ||||
| @@ -18,7 +18,7 @@ class SnNewsSource with _$SnNewsSource { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnNewsArticle with _$SnNewsArticle { | ||||
| abstract class SnNewsArticle with _$SnNewsArticle { | ||||
|   const factory SnNewsArticle({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,8 +6,8 @@ part of 'news.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnNewsSourceImpl( | ||||
| _SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) => | ||||
|     _SnNewsSource( | ||||
|       id: json['id'] as String, | ||||
|       label: json['label'] as String, | ||||
|       type: json['type'] as String, | ||||
| @@ -16,7 +16,7 @@ _$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) => | ||||
|       enabled: json['enabled'] as bool, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) => | ||||
| Map<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'label': instance.label, | ||||
| @@ -26,8 +26,8 @@ Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) => | ||||
|       'enabled': instance.enabled, | ||||
|     }; | ||||
|  | ||||
| _$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnNewsArticleImpl( | ||||
| _SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) => | ||||
|     _SnNewsArticle( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -44,7 +44,7 @@ _$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) => | ||||
|           : DateTime.parse(json['published_at'] as String), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) => | ||||
| Map<String, dynamic> _$SnNewsArticleToJson(_SnNewsArticle instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -4,7 +4,7 @@ part 'notification.freezed.dart'; | ||||
| part 'notification.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnNotification with _$SnNotification { | ||||
| abstract class SnNotification with _$SnNotification { | ||||
|   const factory SnNotification({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| // dart format width=80 | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| @@ -9,156 +10,92 @@ part of 'notification.dart'; | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| final _privateConstructorUsedError = UnsupportedError( | ||||
|     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); | ||||
|  | ||||
| SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) { | ||||
|   return _SnNotification.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnNotification { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get topic => throw _privateConstructorUsedError; | ||||
|   String get title => throw _privateConstructorUsedError; | ||||
|   String? get subtitle => throw _privateConstructorUsedError; | ||||
|   String get body => throw _privateConstructorUsedError; | ||||
|   Map<String, dynamic> get metadata => throw _privateConstructorUsedError; | ||||
|   int get priority => throw _privateConstructorUsedError; | ||||
|   int? get senderId => throw _privateConstructorUsedError; | ||||
|   int get accountId => throw _privateConstructorUsedError; | ||||
|   DateTime? get readAt => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnNotification to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|   int get id; | ||||
|   DateTime get createdAt; | ||||
|   DateTime get updatedAt; | ||||
|   DateTime? get deletedAt; | ||||
|   String get topic; | ||||
|   String get title; | ||||
|   String? get subtitle; | ||||
|   String get body; | ||||
|   Map<String, dynamic> get metadata; | ||||
|   int get priority; | ||||
|   int? get senderId; | ||||
|   int get accountId; | ||||
|   DateTime? get readAt; | ||||
|  | ||||
|   /// Create a copy of SnNotification | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnNotificationCopyWith<SnNotification> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnNotificationCopyWith<$Res> { | ||||
|   factory $SnNotificationCopyWith( | ||||
|           SnNotification value, $Res Function(SnNotification) then) = | ||||
|       _$SnNotificationCopyWithImpl<$Res, SnNotification>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String topic, | ||||
|       String title, | ||||
|       String? subtitle, | ||||
|       String body, | ||||
|       Map<String, dynamic> metadata, | ||||
|       int priority, | ||||
|       int? senderId, | ||||
|       int accountId, | ||||
|       DateTime? readAt}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnNotificationCopyWithImpl<$Res, $Val extends SnNotification> | ||||
|     implements $SnNotificationCopyWith<$Res> { | ||||
|   _$SnNotificationCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnNotification | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnNotificationCopyWith<SnNotification> get copyWith => | ||||
|       _$SnNotificationCopyWithImpl<SnNotification>( | ||||
|           this as SnNotification, _$identity); | ||||
|  | ||||
|   /// Serializes this SnNotification to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? topic = null, | ||||
|     Object? title = null, | ||||
|     Object? subtitle = freezed, | ||||
|     Object? body = null, | ||||
|     Object? metadata = null, | ||||
|     Object? priority = null, | ||||
|     Object? senderId = freezed, | ||||
|     Object? accountId = null, | ||||
|     Object? readAt = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       topic: null == topic | ||||
|           ? _value.topic | ||||
|           : topic // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: null == title | ||||
|           ? _value.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       subtitle: freezed == subtitle | ||||
|           ? _value.subtitle | ||||
|           : subtitle // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       body: null == body | ||||
|           ? _value.body | ||||
|           : body // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       metadata: null == metadata | ||||
|           ? _value.metadata | ||||
|           : metadata // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|       priority: null == priority | ||||
|           ? _value.priority | ||||
|           : priority // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       senderId: freezed == senderId | ||||
|           ? _value.senderId | ||||
|           : senderId // ignore: cast_nullable_to_non_nullable | ||||
|               as int?, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       readAt: freezed == readAt | ||||
|           ? _value.readAt | ||||
|           : readAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|     ) as $Val); | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is SnNotification && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.topic, topic) || other.topic == topic) && | ||||
|             (identical(other.title, title) || other.title == title) && | ||||
|             (identical(other.subtitle, subtitle) || | ||||
|                 other.subtitle == subtitle) && | ||||
|             (identical(other.body, body) || other.body == body) && | ||||
|             const DeepCollectionEquality().equals(other.metadata, metadata) && | ||||
|             (identical(other.priority, priority) || | ||||
|                 other.priority == priority) && | ||||
|             (identical(other.senderId, senderId) || | ||||
|                 other.senderId == senderId) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId) && | ||||
|             (identical(other.readAt, readAt) || other.readAt == readAt)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       id, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       deletedAt, | ||||
|       topic, | ||||
|       title, | ||||
|       subtitle, | ||||
|       body, | ||||
|       const DeepCollectionEquality().hash(metadata), | ||||
|       priority, | ||||
|       senderId, | ||||
|       accountId, | ||||
|       readAt); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnNotification(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, topic: $topic, title: $title, subtitle: $subtitle, body: $body, metadata: $metadata, priority: $priority, senderId: $senderId, accountId: $accountId, readAt: $readAt)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnNotificationImplCopyWith<$Res> | ||||
|     implements $SnNotificationCopyWith<$Res> { | ||||
|   factory _$$SnNotificationImplCopyWith(_$SnNotificationImpl value, | ||||
|           $Res Function(_$SnNotificationImpl) then) = | ||||
|       __$$SnNotificationImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
| abstract mixin class $SnNotificationCopyWith<$Res> { | ||||
|   factory $SnNotificationCopyWith( | ||||
|           SnNotification value, $Res Function(SnNotification) _then) = | ||||
|       _$SnNotificationCopyWithImpl; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
| @@ -177,12 +114,12 @@ abstract class _$$SnNotificationImplCopyWith<$Res> | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnNotificationImplCopyWithImpl<$Res> | ||||
|     extends _$SnNotificationCopyWithImpl<$Res, _$SnNotificationImpl> | ||||
|     implements _$$SnNotificationImplCopyWith<$Res> { | ||||
|   __$$SnNotificationImplCopyWithImpl( | ||||
|       _$SnNotificationImpl _value, $Res Function(_$SnNotificationImpl) _then) | ||||
|       : super(_value, _then); | ||||
| class _$SnNotificationCopyWithImpl<$Res> | ||||
|     implements $SnNotificationCopyWith<$Res> { | ||||
|   _$SnNotificationCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnNotification _self; | ||||
|   final $Res Function(SnNotification) _then; | ||||
|  | ||||
|   /// Create a copy of SnNotification | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -203,57 +140,57 @@ class __$$SnNotificationImplCopyWithImpl<$Res> | ||||
|     Object? accountId = null, | ||||
|     Object? readAt = freezed, | ||||
|   }) { | ||||
|     return _then(_$SnNotificationImpl( | ||||
|     return _then(_self.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           ? _self.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           ? _self.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           ? _self.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       topic: null == topic | ||||
|           ? _value.topic | ||||
|           ? _self.topic | ||||
|           : topic // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: null == title | ||||
|           ? _value.title | ||||
|           ? _self.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       subtitle: freezed == subtitle | ||||
|           ? _value.subtitle | ||||
|           ? _self.subtitle | ||||
|           : subtitle // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       body: null == body | ||||
|           ? _value.body | ||||
|           ? _self.body | ||||
|           : body // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       metadata: null == metadata | ||||
|           ? _value._metadata | ||||
|           ? _self.metadata | ||||
|           : metadata // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|       priority: null == priority | ||||
|           ? _value.priority | ||||
|           ? _self.priority | ||||
|           : priority // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       senderId: freezed == senderId | ||||
|           ? _value.senderId | ||||
|           ? _self.senderId | ||||
|           : senderId // ignore: cast_nullable_to_non_nullable | ||||
|               as int?, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           ? _self.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       readAt: freezed == readAt | ||||
|           ? _value.readAt | ||||
|           ? _self.readAt | ||||
|           : readAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|     )); | ||||
| @@ -262,8 +199,8 @@ class __$$SnNotificationImplCopyWithImpl<$Res> | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnNotificationImpl implements _SnNotification { | ||||
|   const _$SnNotificationImpl( | ||||
| class _SnNotification implements SnNotification { | ||||
|   const _SnNotification( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
| @@ -278,9 +215,8 @@ class _$SnNotificationImpl implements _SnNotification { | ||||
|       required this.accountId, | ||||
|       required this.readAt}) | ||||
|       : _metadata = metadata; | ||||
|  | ||||
|   factory _$SnNotificationImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnNotificationImplFromJson(json); | ||||
|   factory _SnNotification.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnNotificationFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
| @@ -316,16 +252,26 @@ class _$SnNotificationImpl implements _SnNotification { | ||||
|   @override | ||||
|   final DateTime? readAt; | ||||
|  | ||||
|   /// Create a copy of SnNotification | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnNotification(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, topic: $topic, title: $title, subtitle: $subtitle, body: $body, metadata: $metadata, priority: $priority, senderId: $senderId, accountId: $accountId, readAt: $readAt)'; | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$SnNotificationCopyWith<_SnNotification> get copyWith => | ||||
|       __$SnNotificationCopyWithImpl<_SnNotification>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$SnNotificationToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnNotificationImpl && | ||||
|             other is _SnNotification && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
| @@ -366,73 +312,118 @@ class _$SnNotificationImpl implements _SnNotification { | ||||
|       accountId, | ||||
|       readAt); | ||||
|  | ||||
|   /// Create a copy of SnNotification | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnNotificationImplCopyWith<_$SnNotificationImpl> get copyWith => | ||||
|       __$$SnNotificationImplCopyWithImpl<_$SnNotificationImpl>( | ||||
|           this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnNotificationImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   String toString() { | ||||
|     return 'SnNotification(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, topic: $topic, title: $title, subtitle: $subtitle, body: $body, metadata: $metadata, priority: $priority, senderId: $senderId, accountId: $accountId, readAt: $readAt)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnNotification implements SnNotification { | ||||
|   const factory _SnNotification( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final String topic, | ||||
|       required final String title, | ||||
|       required final String? subtitle, | ||||
|       required final String body, | ||||
|       final Map<String, dynamic> metadata, | ||||
|       required final int priority, | ||||
|       required final int? senderId, | ||||
|       required final int accountId, | ||||
|       required final DateTime? readAt}) = _$SnNotificationImpl; | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnNotificationCopyWith<$Res> | ||||
|     implements $SnNotificationCopyWith<$Res> { | ||||
|   factory _$SnNotificationCopyWith( | ||||
|           _SnNotification value, $Res Function(_SnNotification) _then) = | ||||
|       __$SnNotificationCopyWithImpl; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String topic, | ||||
|       String title, | ||||
|       String? subtitle, | ||||
|       String body, | ||||
|       Map<String, dynamic> metadata, | ||||
|       int priority, | ||||
|       int? senderId, | ||||
|       int accountId, | ||||
|       DateTime? readAt}); | ||||
| } | ||||
|  | ||||
|   factory _SnNotification.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnNotificationImpl.fromJson; | ||||
| /// @nodoc | ||||
| class __$SnNotificationCopyWithImpl<$Res> | ||||
|     implements _$SnNotificationCopyWith<$Res> { | ||||
|   __$SnNotificationCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   @override | ||||
|   DateTime? get deletedAt; | ||||
|   @override | ||||
|   String get topic; | ||||
|   @override | ||||
|   String get title; | ||||
|   @override | ||||
|   String? get subtitle; | ||||
|   @override | ||||
|   String get body; | ||||
|   @override | ||||
|   Map<String, dynamic> get metadata; | ||||
|   @override | ||||
|   int get priority; | ||||
|   @override | ||||
|   int? get senderId; | ||||
|   @override | ||||
|   int get accountId; | ||||
|   @override | ||||
|   DateTime? get readAt; | ||||
|   final _SnNotification _self; | ||||
|   final $Res Function(_SnNotification) _then; | ||||
|  | ||||
|   /// Create a copy of SnNotification | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnNotificationImplCopyWith<_$SnNotificationImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? topic = null, | ||||
|     Object? title = null, | ||||
|     Object? subtitle = freezed, | ||||
|     Object? body = null, | ||||
|     Object? metadata = null, | ||||
|     Object? priority = null, | ||||
|     Object? senderId = freezed, | ||||
|     Object? accountId = null, | ||||
|     Object? readAt = freezed, | ||||
|   }) { | ||||
|     return _then(_SnNotification( | ||||
|       id: null == id | ||||
|           ? _self.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       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, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _self.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       topic: null == topic | ||||
|           ? _self.topic | ||||
|           : topic // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: null == title | ||||
|           ? _self.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       subtitle: freezed == subtitle | ||||
|           ? _self.subtitle | ||||
|           : subtitle // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       body: null == body | ||||
|           ? _self.body | ||||
|           : body // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       metadata: null == metadata | ||||
|           ? _self._metadata | ||||
|           : metadata // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|       priority: null == priority | ||||
|           ? _self.priority | ||||
|           : priority // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       senderId: freezed == senderId | ||||
|           ? _self.senderId | ||||
|           : senderId // ignore: cast_nullable_to_non_nullable | ||||
|               as int?, | ||||
|       accountId: null == accountId | ||||
|           ? _self.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       readAt: freezed == readAt | ||||
|           ? _self.readAt | ||||
|           : readAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
|   | ||||
| @@ -6,8 +6,8 @@ part of 'notification.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnNotificationImpl _$$SnNotificationImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnNotificationImpl( | ||||
| _SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) => | ||||
|     _SnNotification( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -27,8 +27,7 @@ _$SnNotificationImpl _$$SnNotificationImplFromJson(Map<String, dynamic> json) => | ||||
|           : DateTime.parse(json['read_at'] as String), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnNotificationImplToJson( | ||||
|         _$SnNotificationImpl instance) => | ||||
| Map<String, dynamic> _$SnNotificationToJson(_SnNotification instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -4,7 +4,7 @@ part 'poll.freezed.dart'; | ||||
| part 'poll.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnPoll with _$SnPoll { | ||||
| abstract class SnPoll with _$SnPoll { | ||||
|   const factory SnPoll({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -20,7 +20,7 @@ class SnPoll with _$SnPoll { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnPollMetric with _$SnPollMetric { | ||||
| abstract class SnPollMetric with _$SnPollMetric { | ||||
|   const factory SnPollMetric({ | ||||
|     required int totalAnswer, | ||||
|     @Default({}) Map<String, int> byOptions, | ||||
| @@ -32,7 +32,7 @@ class SnPollMetric with _$SnPollMetric { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnPollOption with _$SnPollOption { | ||||
| abstract class SnPollOption with _$SnPollOption { | ||||
|   const factory SnPollOption({ | ||||
|     required String id, | ||||
|     required String icon, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,7 +6,7 @@ part of 'poll.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnPollImpl _$$SnPollImplFromJson(Map<String, dynamic> json) => _$SnPollImpl( | ||||
| _SnPoll _$SnPollFromJson(Map<String, dynamic> json) => _SnPoll( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -19,8 +19,7 @@ _$SnPollImpl _$$SnPollImplFromJson(Map<String, dynamic> json) => _$SnPollImpl( | ||||
|       metric: SnPollMetric.fromJson(json['metric'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) => | ||||
|     <String, dynamic>{ | ||||
| Map<String, dynamic> _$SnPollToJson(_SnPoll instance) => <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
| @@ -31,8 +30,8 @@ Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) => | ||||
|       'metric': instance.metric.toJson(), | ||||
|     }; | ||||
|  | ||||
| _$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnPollMetricImpl( | ||||
| _SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) => | ||||
|     _SnPollMetric( | ||||
|       totalAnswer: (json['total_answer'] as num).toInt(), | ||||
|       byOptions: (json['by_options'] as Map<String, dynamic>?)?.map( | ||||
|             (k, e) => MapEntry(k, (e as num).toInt()), | ||||
| @@ -45,22 +44,22 @@ _$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) => | ||||
|               const {}, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) => | ||||
| Map<String, dynamic> _$SnPollMetricToJson(_SnPollMetric instance) => | ||||
|     <String, dynamic>{ | ||||
|       'total_answer': instance.totalAnswer, | ||||
|       'by_options': instance.byOptions, | ||||
|       'by_options_percentage': instance.byOptionsPercentage, | ||||
|     }; | ||||
|  | ||||
| _$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnPollOptionImpl( | ||||
| _SnPollOption _$SnPollOptionFromJson(Map<String, dynamic> json) => | ||||
|     _SnPollOption( | ||||
|       id: json['id'] as String, | ||||
|       icon: json['icon'] as String, | ||||
|       name: json['name'] as String, | ||||
|       description: json['description'] as String, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPollOptionImplToJson(_$SnPollOptionImpl instance) => | ||||
| Map<String, dynamic> _$SnPollOptionToJson(_SnPollOption instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'icon': instance.icon, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part 'post.freezed.dart'; | ||||
| part 'post.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnPost with _$SnPost { | ||||
| abstract class SnPost with _$SnPost { | ||||
|   const SnPost._(); | ||||
|  | ||||
|   const factory SnPost({ | ||||
| @@ -57,7 +57,7 @@ class SnPost with _$SnPost { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnPostTag with _$SnPostTag { | ||||
| abstract class SnPostTag with _$SnPostTag { | ||||
|   const factory SnPostTag({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -74,7 +74,7 @@ class SnPostTag with _$SnPostTag { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnPostCategory with _$SnPostCategory { | ||||
| abstract class SnPostCategory with _$SnPostCategory { | ||||
|   const factory SnPostCategory({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -91,7 +91,7 @@ class SnPostCategory with _$SnPostCategory { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnPostPreload with _$SnPostPreload { | ||||
| abstract class SnPostPreload with _$SnPostPreload { | ||||
|   const factory SnPostPreload({ | ||||
|     required SnAttachment? thumbnail, | ||||
|     required List<SnAttachment?>? attachments, | ||||
| @@ -105,7 +105,7 @@ class SnPostPreload with _$SnPostPreload { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnBody with _$SnBody { | ||||
| abstract class SnBody with _$SnBody { | ||||
|   const factory SnBody({ | ||||
|     required List<String> attachments, | ||||
|     required String content, | ||||
| @@ -118,7 +118,7 @@ class SnBody with _$SnBody { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnMetric with _$SnMetric { | ||||
| abstract class SnMetric with _$SnMetric { | ||||
|   const factory SnMetric({ | ||||
|     required int replyCount, | ||||
|     required int reactionCount, | ||||
| @@ -130,7 +130,7 @@ class SnMetric with _$SnMetric { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnPublisher with _$SnPublisher { | ||||
| abstract class SnPublisher with _$SnPublisher { | ||||
|   const factory SnPublisher({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -153,7 +153,7 @@ class SnPublisher with _$SnPublisher { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnSubscription with _$SnSubscription { | ||||
| abstract class SnSubscription with _$SnSubscription { | ||||
|   const factory SnSubscription({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,7 +6,7 @@ part of 'post.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl( | ||||
| _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -76,8 +76,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl( | ||||
|           : SnPostPreload.fromJson(json['preload'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) => | ||||
|     <String, dynamic>{ | ||||
| Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
| @@ -115,8 +114,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) => | ||||
|       'preload': instance.preload?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _$SnPostTagImpl _$$SnPostTagImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnPostTagImpl( | ||||
| _SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -127,7 +125,7 @@ _$SnPostTagImpl _$$SnPostTagImplFromJson(Map<String, dynamic> json) => | ||||
|       posts: json['posts'], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPostTagImplToJson(_$SnPostTagImpl instance) => | ||||
| Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -139,8 +137,8 @@ Map<String, dynamic> _$$SnPostTagImplToJson(_$SnPostTagImpl instance) => | ||||
|       'posts': instance.posts, | ||||
|     }; | ||||
|  | ||||
| _$SnPostCategoryImpl _$$SnPostCategoryImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnPostCategoryImpl( | ||||
| _SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) => | ||||
|     _SnPostCategory( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -151,8 +149,7 @@ _$SnPostCategoryImpl _$$SnPostCategoryImplFromJson(Map<String, dynamic> json) => | ||||
|       posts: json['posts'], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPostCategoryImplToJson( | ||||
|         _$SnPostCategoryImpl instance) => | ||||
| Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -164,8 +161,8 @@ Map<String, dynamic> _$$SnPostCategoryImplToJson( | ||||
|       'posts': instance.posts, | ||||
|     }; | ||||
|  | ||||
| _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnPostPreloadImpl( | ||||
| _SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) => | ||||
|     _SnPostPreload( | ||||
|       thumbnail: json['thumbnail'] == null | ||||
|           ? null | ||||
|           : SnAttachment.fromJson(json['thumbnail'] as Map<String, dynamic>), | ||||
| @@ -185,7 +182,7 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) => | ||||
|           : SnRealm.fromJson(json['realm'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) => | ||||
| Map<String, dynamic> _$SnPostPreloadToJson(_SnPostPreload instance) => | ||||
|     <String, dynamic>{ | ||||
|       'thumbnail': instance.thumbnail?.toJson(), | ||||
|       'attachments': instance.attachments?.map((e) => e?.toJson()).toList(), | ||||
| @@ -194,7 +191,7 @@ Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) => | ||||
|       'realm': instance.realm?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl( | ||||
| _SnBody _$SnBodyFromJson(Map<String, dynamic> json) => _SnBody( | ||||
|       attachments: (json['attachments'] as List<dynamic>) | ||||
|           .map((e) => e as String) | ||||
|           .toList(), | ||||
| @@ -204,8 +201,7 @@ _$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl( | ||||
|       title: json['title'], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnBodyImplToJson(_$SnBodyImpl instance) => | ||||
|     <String, dynamic>{ | ||||
| Map<String, dynamic> _$SnBodyToJson(_SnBody instance) => <String, dynamic>{ | ||||
|       'attachments': instance.attachments, | ||||
|       'content': instance.content, | ||||
|       'location': instance.location, | ||||
| @@ -213,8 +209,7 @@ Map<String, dynamic> _$$SnBodyImplToJson(_$SnBodyImpl instance) => | ||||
|       'title': instance.title, | ||||
|     }; | ||||
|  | ||||
| _$SnMetricImpl _$$SnMetricImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnMetricImpl( | ||||
| _SnMetric _$SnMetricFromJson(Map<String, dynamic> json) => _SnMetric( | ||||
|       replyCount: (json['reply_count'] as num).toInt(), | ||||
|       reactionCount: (json['reaction_count'] as num).toInt(), | ||||
|       reactionList: (json['reaction_list'] as Map<String, dynamic>?)?.map( | ||||
| @@ -223,15 +218,13 @@ _$SnMetricImpl _$$SnMetricImplFromJson(Map<String, dynamic> json) => | ||||
|           const {}, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnMetricImplToJson(_$SnMetricImpl instance) => | ||||
|     <String, dynamic>{ | ||||
| Map<String, dynamic> _$SnMetricToJson(_SnMetric instance) => <String, dynamic>{ | ||||
|       'reply_count': instance.replyCount, | ||||
|       'reaction_count': instance.reactionCount, | ||||
|       'reaction_list': instance.reactionList, | ||||
|     }; | ||||
|  | ||||
| _$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnPublisherImpl( | ||||
| _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -250,7 +243,7 @@ _$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) => | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnPublisherImplToJson(_$SnPublisherImpl instance) => | ||||
| Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -268,8 +261,8 @@ Map<String, dynamic> _$$SnPublisherImplToJson(_$SnPublisherImpl instance) => | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnSubscriptionImpl _$$SnSubscriptionImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnSubscriptionImpl( | ||||
| _SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) => | ||||
|     _SnSubscription( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -280,8 +273,7 @@ _$SnSubscriptionImpl _$$SnSubscriptionImplFromJson(Map<String, dynamic> json) => | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnSubscriptionImplToJson( | ||||
|         _$SnSubscriptionImpl instance) => | ||||
| Map<String, dynamic> _$SnSubscriptionToJson(_SnSubscription instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -5,7 +5,7 @@ part 'realm.freezed.dart'; | ||||
| part 'realm.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnRealmMember with _$SnRealmMember { | ||||
| abstract class SnRealmMember with _$SnRealmMember { | ||||
|   const factory SnRealmMember({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -23,7 +23,7 @@ class SnRealmMember with _$SnRealmMember { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnRealm with _$SnRealm { | ||||
| abstract class SnRealm with _$SnRealm { | ||||
|   const SnRealm._(); | ||||
|  | ||||
|   const factory SnRealm({ | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,8 +6,8 @@ part of 'realm.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnRealmMemberImpl _$$SnRealmMemberImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnRealmMemberImpl( | ||||
| _SnRealmMember _$SnRealmMemberFromJson(Map<String, dynamic> json) => | ||||
|     _SnRealmMember( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -21,7 +21,7 @@ _$SnRealmMemberImpl _$$SnRealmMemberImplFromJson(Map<String, dynamic> json) => | ||||
|       powerLevel: (json['power_level'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnRealmMemberImplToJson(_$SnRealmMemberImpl instance) => | ||||
| Map<String, dynamic> _$SnRealmMemberToJson(_SnRealmMember instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
| @@ -34,8 +34,7 @@ Map<String, dynamic> _$$SnRealmMemberImplToJson(_$SnRealmMemberImpl instance) => | ||||
|       'power_level': instance.powerLevel, | ||||
|     }; | ||||
|  | ||||
| _$SnRealmImpl _$$SnRealmImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnRealmImpl( | ||||
| _SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -57,8 +56,7 @@ _$SnRealmImpl _$$SnRealmImplFromJson(Map<String, dynamic> json) => | ||||
|       popularity: (json['popularity'] as num?)?.toInt() ?? 0, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnRealmImplToJson(_$SnRealmImpl instance) => | ||||
|     <String, dynamic>{ | ||||
| Map<String, dynamic> _$SnRealmToJson(_SnRealm instance) => <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|   | ||||
| @@ -4,7 +4,7 @@ part 'wallet.freezed.dart'; | ||||
| part 'wallet.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnWallet with _$SnWallet { | ||||
| abstract class SnWallet with _$SnWallet { | ||||
|   const factory SnWallet({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
| @@ -19,7 +19,7 @@ class SnWallet with _$SnWallet { | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnTransaction with _$SnTransaction { | ||||
| abstract class SnTransaction with _$SnTransaction { | ||||
|   const factory SnTransaction({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,8 +6,7 @@ part of 'wallet.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnWalletImpl _$$SnWalletImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnWalletImpl( | ||||
| _SnWallet _$SnWalletFromJson(Map<String, dynamic> json) => _SnWallet( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -19,8 +18,7 @@ _$SnWalletImpl _$$SnWalletImplFromJson(Map<String, dynamic> json) => | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnWalletImplToJson(_$SnWalletImpl instance) => | ||||
|     <String, dynamic>{ | ||||
| Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
| @@ -30,8 +28,8 @@ Map<String, dynamic> _$$SnWalletImplToJson(_$SnWalletImpl instance) => | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnTransactionImpl _$$SnTransactionImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnTransactionImpl( | ||||
| _SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) => | ||||
|     _SnTransaction( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -50,7 +48,7 @@ _$SnTransactionImpl _$$SnTransactionImplFromJson(Map<String, dynamic> json) => | ||||
|       payeeId: (json['payee_id'] as num?)?.toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnTransactionImplToJson(_$SnTransactionImpl instance) => | ||||
| Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|   | ||||
| @@ -4,7 +4,7 @@ part 'websocket.freezed.dart'; | ||||
| part 'websocket.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class WebSocketPackage with _$WebSocketPackage { | ||||
| abstract class WebSocketPackage with _$WebSocketPackage { | ||||
|   const factory WebSocketPackage({ | ||||
|     @JsonKey(name: 'w') @Default('unknown') String method, | ||||
|     @JsonKey(name: 'e') String? endpoint, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user