Compare commits
	
		
			59 Commits
		
	
	
		
			2.3.2+73
			...
			54c098c274
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| acbc125dec | |||
| ad0ee971c1 | |||
| 52d6bb083e | |||
| 2027eab49b | |||
| 566ebde1dd | |||
| 9e039cc532 | |||
| c4b95d7084 | |||
| a66129a9ba | |||
| 44e1a8bf67 | 
							
								
								
									
										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.
										
									
								
							@@ -203,6 +203,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 +517,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",
 | 
			
		||||
@@ -719,7 +729,39 @@
 | 
			
		||||
  "stickersNewDescription": "Create a new sticker belongs to this pack.",
 | 
			
		||||
  "stickersPackNew": "New Sticker Pack",
 | 
			
		||||
  "trayMenuShow": "Show",
 | 
			
		||||
  "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"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -201,6 +201,11 @@
 | 
			
		||||
    "other": "{} 条评论"
 | 
			
		||||
  },
 | 
			
		||||
  "settingsAppearance": "外观",
 | 
			
		||||
  "settingsCustomFonts": "自定义字体",
 | 
			
		||||
  "settingsCustomFontsDescription": "设置应用程序使用的字体。",
 | 
			
		||||
  "settingsCustomFontFamily": "应用字体",
 | 
			
		||||
  "settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高",
 | 
			
		||||
  "settingsCustomFontApplied": "自定义字体已经应用。",
 | 
			
		||||
  "settingsDisplayLanguage": "显示语言",
 | 
			
		||||
  "settingsDisplayLanguageDescription": "设置应用程序使用的语言",
 | 
			
		||||
  "settingsDisplayLanguageSystem": "跟随系统",
 | 
			
		||||
@@ -510,8 +515,13 @@
 | 
			
		||||
  "accountBirthday": "出生于 {}",
 | 
			
		||||
  "accountBadge": "徽章",
 | 
			
		||||
  "accountCheckInNoRecords": "暂无运势记录",
 | 
			
		||||
  "badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
 | 
			
		||||
  "badgeCompanyStaff": "工作人员",
 | 
			
		||||
  "badgeSiteMigration": "Solar Network 原住民",
 | 
			
		||||
  "badgeCommunitySurvey": "调研参与者",
 | 
			
		||||
  "badgeCommunityVerified": "认证用户",
 | 
			
		||||
  "badgeCommunityContributor": "优秀社区贡献者",
 | 
			
		||||
  "badgeSiteAnniversary": "周年纪念",
 | 
			
		||||
  "badgeUserBirthday": "生日纪念",
 | 
			
		||||
  "accountStatus": "状态",
 | 
			
		||||
  "accountStatusOnline": "在线",
 | 
			
		||||
  "accountStatusOffline": "离线",
 | 
			
		||||
@@ -717,7 +727,39 @@
 | 
			
		||||
  "stickersNewDescription": "创建一个新的贴图。",
 | 
			
		||||
  "stickersPackNew": "新建贴图包",
 | 
			
		||||
  "trayMenuShow": "显示",
 | 
			
		||||
  "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": "无法预览加密消息"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -201,6 +201,11 @@
 | 
			
		||||
    "other": "{} 條評論"
 | 
			
		||||
  },
 | 
			
		||||
  "settingsAppearance": "外觀",
 | 
			
		||||
  "settingsCustomFonts": "自定義字體",
 | 
			
		||||
  "settingsCustomFontsDescription": "設置應用程序使用的字體。",
 | 
			
		||||
  "settingsCustomFontFamily": "應用字體",
 | 
			
		||||
  "settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
 | 
			
		||||
  "settingsCustomFontApplied": "自定義字體已經應用。",
 | 
			
		||||
  "settingsDisplayLanguage": "顯示語言",
 | 
			
		||||
  "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
 | 
			
		||||
  "settingsDisplayLanguageSystem": "跟隨系統",
 | 
			
		||||
@@ -510,8 +515,13 @@
 | 
			
		||||
  "accountBirthday": "出生於 {}",
 | 
			
		||||
  "accountBadge": "徽章",
 | 
			
		||||
  "accountCheckInNoRecords": "暫無運勢記錄",
 | 
			
		||||
  "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
 | 
			
		||||
  "badgeCompanyStaff": "工作人員",
 | 
			
		||||
  "badgeSiteMigration": "Solar Network 原住民",
 | 
			
		||||
  "badgeCommunitySurvey": "調研參與者",
 | 
			
		||||
  "badgeCommunityVerified": "認證用户",
 | 
			
		||||
  "badgeCommunityContributor": "優秀社區貢獻者",
 | 
			
		||||
  "badgeSiteAnniversary": "週年紀念",
 | 
			
		||||
  "badgeUserBirthday": "生日紀念",
 | 
			
		||||
  "accountStatus": "狀態",
 | 
			
		||||
  "accountStatusOnline": "在線",
 | 
			
		||||
  "accountStatusOffline": "離線",
 | 
			
		||||
@@ -717,7 +727,39 @@
 | 
			
		||||
  "stickersNewDescription": "創建一個新的貼圖。",
 | 
			
		||||
  "stickersPackNew": "新建貼圖包",
 | 
			
		||||
  "trayMenuShow": "顯示",
 | 
			
		||||
  "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": "無法預覽加密消息"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -201,6 +201,11 @@
 | 
			
		||||
    "other": "{} 條評論"
 | 
			
		||||
  },
 | 
			
		||||
  "settingsAppearance": "外觀",
 | 
			
		||||
  "settingsCustomFonts": "自定義字體",
 | 
			
		||||
  "settingsCustomFontsDescription": "設置應用程序使用的字體。",
 | 
			
		||||
  "settingsCustomFontFamily": "應用字體",
 | 
			
		||||
  "settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
 | 
			
		||||
  "settingsCustomFontApplied": "自定義字體已經應用。",
 | 
			
		||||
  "settingsDisplayLanguage": "顯示語言",
 | 
			
		||||
  "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
 | 
			
		||||
  "settingsDisplayLanguageSystem": "跟隨系統",
 | 
			
		||||
@@ -510,8 +515,13 @@
 | 
			
		||||
  "accountBirthday": "出生於 {}",
 | 
			
		||||
  "accountBadge": "徽章",
 | 
			
		||||
  "accountCheckInNoRecords": "暫無運勢記錄",
 | 
			
		||||
  "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
 | 
			
		||||
  "badgeCompanyStaff": "工作人員",
 | 
			
		||||
  "badgeSiteMigration": "Solar Network 原住民",
 | 
			
		||||
  "badgeCommunitySurvey": "調研參與者",
 | 
			
		||||
  "badgeCommunityVerified": "認證用戶",
 | 
			
		||||
  "badgeCommunityContributor": "優秀社區貢獻者",
 | 
			
		||||
  "badgeSiteAnniversary": "週年紀念",
 | 
			
		||||
  "badgeUserBirthday": "生日紀念",
 | 
			
		||||
  "accountStatus": "狀態",
 | 
			
		||||
  "accountStatusOnline": "在線",
 | 
			
		||||
  "accountStatusOffline": "離線",
 | 
			
		||||
@@ -717,7 +727,39 @@
 | 
			
		||||
  "stickersNewDescription": "創建一個新的貼圖。",
 | 
			
		||||
  "stickersPackNew": "新建貼圖包",
 | 
			
		||||
  "trayMenuShow": "顯示",
 | 
			
		||||
  "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": "無法預覽加密消息"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,3 +5,7 @@ targets:
 | 
			
		||||
        options:
 | 
			
		||||
          explicit_to_json: true
 | 
			
		||||
          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();
 | 
			
		||||
 | 
			
		||||
@@ -194,9 +199,11 @@ class ChatMessageController extends ChangeNotifier {
 | 
			
		||||
          channelId: channel!.id,
 | 
			
		||||
          createdAt: Value(message.createdAt),
 | 
			
		||||
        ),
 | 
			
		||||
        onConflict: DoUpdate((_) => SnLocalChatMessageCompanion.custom(
 | 
			
		||||
              content: Constant(jsonEncode(message.toJson())),
 | 
			
		||||
            )),
 | 
			
		||||
        onConflict: DoUpdate(
 | 
			
		||||
          (_) => SnLocalChatMessageCompanion.custom(
 | 
			
		||||
            content: Constant(jsonEncode(message.toJson())),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      incomeStrandedQueue.add(message);
 | 
			
		||||
@@ -212,21 +219,21 @@ class ChatMessageController extends ChangeNotifier {
 | 
			
		||||
          final idx =
 | 
			
		||||
              messages.indexWhere((x) => x.id == message.relatedEventId);
 | 
			
		||||
          if (idx != -1) {
 | 
			
		||||
            final newBody = message.body;
 | 
			
		||||
            final newBody = Map<String, dynamic>.from(message.body);
 | 
			
		||||
            newBody.remove('related_event');
 | 
			
		||||
            messages[idx] = messages[idx].copyWith(
 | 
			
		||||
              body: newBody,
 | 
			
		||||
              updatedAt: message.updatedAt,
 | 
			
		||||
            );
 | 
			
		||||
            if (message.relatedEventId != null) {
 | 
			
		||||
              await (_dt.db.snLocalChatMessage.update()
 | 
			
		||||
                    ..where((e) => e.id.equals(message.relatedEventId!)))
 | 
			
		||||
                  .write(
 | 
			
		||||
                SnLocalChatMessageCompanion.custom(
 | 
			
		||||
                  content: Constant(jsonEncode(messages[idx].toJson())),
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (message.relatedEventId != null) {
 | 
			
		||||
            await (_dt.db.snLocalChatMessage.update()
 | 
			
		||||
                  ..where((e) => e.id.equals(message.relatedEventId!)))
 | 
			
		||||
                .write(
 | 
			
		||||
              SnLocalChatMessageCompanion.custom(
 | 
			
		||||
                content: Constant(jsonEncode(messages[idx].toJson())),
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      case 'messages.delete':
 | 
			
		||||
@@ -241,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, {
 | 
			
		||||
@@ -248,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)
 | 
			
		||||
@@ -262,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 {
 | 
			
		||||
@@ -318,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()
 | 
			
		||||
@@ -332,6 +360,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
			
		||||
    if (mostRecentMessage == null) {
 | 
			
		||||
      // Initial load
 | 
			
		||||
      await loadMessages(take: 20);
 | 
			
		||||
      isAggressiveLoading = false;
 | 
			
		||||
      isCheckedUpdate = true;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -349,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((_) {
 | 
			
		||||
@@ -530,7 +565,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
			
		||||
        },
 | 
			
		||||
      ).toJson(),
 | 
			
		||||
    ));
 | 
			
		||||
    log('[Messaging] Send read event request: $_readEventAnchor');
 | 
			
		||||
    logging.debug('[Messaging] Send read event request: $_readEventAnchor');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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);
 | 
			
		||||
@@ -333,6 +345,31 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final Menu _appTrayMenu = Menu(
 | 
			
		||||
    items: [
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'version_label',
 | 
			
		||||
        label: 'Solian',
 | 
			
		||||
        disabled: true,
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem.separator(),
 | 
			
		||||
      MenuItem.checkbox(
 | 
			
		||||
        checked: false,
 | 
			
		||||
        key: 'mute_notification',
 | 
			
		||||
        label: 'trayMenuMuteNotification'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem.separator(),
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'window_show',
 | 
			
		||||
        label: 'trayMenuShow'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'exit',
 | 
			
		||||
        label: 'trayMenuExit'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Future<void> _trayInitialization() async {
 | 
			
		||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
			
		||||
 | 
			
		||||
@@ -344,32 +381,20 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
    trayManager.addListener(this);
 | 
			
		||||
    await trayManager.setIcon(icon);
 | 
			
		||||
 | 
			
		||||
    Menu menu = Menu(
 | 
			
		||||
      items: [
 | 
			
		||||
        MenuItem(
 | 
			
		||||
          key: 'version_label',
 | 
			
		||||
          label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
 | 
			
		||||
          disabled: true,
 | 
			
		||||
        ),
 | 
			
		||||
        MenuItem.separator(),
 | 
			
		||||
        MenuItem(
 | 
			
		||||
          key: 'window_show',
 | 
			
		||||
          label: 'trayMenuShow'.tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        MenuItem(
 | 
			
		||||
          key: 'exit',
 | 
			
		||||
          label: 'trayMenuExit'.tr(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    _appTrayMenu.items![0] = MenuItem(
 | 
			
		||||
      key: 'version_label',
 | 
			
		||||
      label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
 | 
			
		||||
      disabled: true,
 | 
			
		||||
    );
 | 
			
		||||
    await trayManager.setContextMenu(menu);
 | 
			
		||||
 | 
			
		||||
    await trayManager.setContextMenu(_appTrayMenu);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _notifyInitialization() async {
 | 
			
		||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
			
		||||
 | 
			
		||||
    await localNotifier.setup(
 | 
			
		||||
      appName: 'solian',
 | 
			
		||||
      appName: 'Solian',
 | 
			
		||||
      shortcutPolicy: ShortcutPolicy.requireCreate,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -424,12 +449,23 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
  @override
 | 
			
		||||
  void onTrayMenuItemClick(MenuItem menuItem) {
 | 
			
		||||
    switch (menuItem.key) {
 | 
			
		||||
      case 'mute_notification':
 | 
			
		||||
        final nty = context.read<NotificationProvider>();
 | 
			
		||||
        nty.isMuted = !nty.isMuted;
 | 
			
		||||
        _appTrayMenu.items![2].checked = nty.isMuted;
 | 
			
		||||
        trayManager.setContextMenu(_appTrayMenu);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'window_show':
 | 
			
		||||
        appWindow.show();
 | 
			
		||||
        // To prevent the window from being hide after just show on macOS
 | 
			
		||||
        Timer(const Duration(milliseconds: 100), () => appWindow.show());
 | 
			
		||||
        break;
 | 
			
		||||
      case 'exit':
 | 
			
		||||
        _appLifecycleListener?.dispose();
 | 
			
		||||
        SystemChannels.platform.invokeMethod('SystemNavigator.pop');
 | 
			
		||||
        if (Platform.isWindows) {
 | 
			
		||||
          appWindow.close();
 | 
			
		||||
        } else {
 | 
			
		||||
          SystemChannels.platform.invokeMethod('SystemNavigator.pop');
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -459,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',
 | 
			
		||||
@@ -79,6 +81,7 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
  List<SnNotification> notifications = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  int? skippableNotifyChannel;
 | 
			
		||||
  bool isMuted = false;
 | 
			
		||||
 | 
			
		||||
  void listen() {
 | 
			
		||||
    _ws.pk.stream.listen((event) {
 | 
			
		||||
@@ -88,7 +91,8 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
        final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
 | 
			
		||||
        if (doHaptic) HapticFeedback.mediumImpact();
 | 
			
		||||
 | 
			
		||||
        if (notification.topic == 'messaging.message') {
 | 
			
		||||
        if (notification.topic == 'messaging.message' &&
 | 
			
		||||
            skippableNotifyChannel != null) {
 | 
			
		||||
          if (notification.metadata['channel_id'] != null &&
 | 
			
		||||
              notification.metadata['channel_id'] == skippableNotifyChannel) {
 | 
			
		||||
            return;
 | 
			
		||||
@@ -106,7 +110,7 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
        updateTray();
 | 
			
		||||
 | 
			
		||||
        if (!kIsWeb) {
 | 
			
		||||
        if (!kIsWeb && !isMuted) {
 | 
			
		||||
          if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
 | 
			
		||||
            LocalNotification notify = LocalNotification(
 | 
			
		||||
              title: notification.title,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,20 +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 {
 | 
			
		||||
  Future<List<SnAttachment?>> getMultiple(List<String> rids,
 | 
			
		||||
      {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++) {
 | 
			
		||||
@@ -52,32 +70,55 @@ 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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Map<String, String> mimetypeOverrides = {'mov': 'video/quicktime', 'mp4': 'video/mp4'};
 | 
			
		||||
  static Map<String, String> mimetypeOverrides = {
 | 
			
		||||
    'mov': 'video/quicktime',
 | 
			
		||||
    'mp4': 'video/mp4',
 | 
			
		||||
    'm4a': 'audio/mp4',
 | 
			
		||||
    'apng': 'image/apng',
 | 
			
		||||
    'webp': 'image/webp',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  Future<SnAttachment> directUploadOne(
 | 
			
		||||
    Uint8List data,
 | 
			
		||||
@@ -89,8 +130,11 @@ class SnAttachmentProvider {
 | 
			
		||||
    bool analyzeNow = false,
 | 
			
		||||
  }) async {
 | 
			
		||||
    final filePayload = MultipartFile.fromBytes(data, filename: filename);
 | 
			
		||||
    final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
 | 
			
		||||
    final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
 | 
			
		||||
    final fileAlt = filename.contains('.')
 | 
			
		||||
        ? filename.substring(0, filename.lastIndexOf('.'))
 | 
			
		||||
        : filename;
 | 
			
		||||
    final fileExt =
 | 
			
		||||
        filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
 | 
			
		||||
 | 
			
		||||
    String? mimetypeOverride;
 | 
			
		||||
    if (mimetype != null) {
 | 
			
		||||
@@ -127,8 +171,11 @@ class SnAttachmentProvider {
 | 
			
		||||
    Map<String, dynamic>? metadata, {
 | 
			
		||||
    String? mimetype,
 | 
			
		||||
  }) async {
 | 
			
		||||
    final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
 | 
			
		||||
    final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
 | 
			
		||||
    final fileAlt = filename.contains('.')
 | 
			
		||||
        ? filename.substring(0, filename.lastIndexOf('.'))
 | 
			
		||||
        : filename;
 | 
			
		||||
    final fileExt =
 | 
			
		||||
        filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
 | 
			
		||||
 | 
			
		||||
    String? mimetypeOverride;
 | 
			
		||||
    if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
 | 
			
		||||
@@ -146,7 +193,10 @@ class SnAttachmentProvider {
 | 
			
		||||
      if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
 | 
			
		||||
    return (
 | 
			
		||||
      SnAttachmentFragment.fromJson(resp.data['meta']),
 | 
			
		||||
      resp.data['chunk_size'] as int
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<dynamic> _chunkedUploadOnePart(
 | 
			
		||||
@@ -197,7 +247,10 @@ class SnAttachmentProvider {
 | 
			
		||||
          (entry.value + 1) * chunkSize,
 | 
			
		||||
          await file.length(),
 | 
			
		||||
        );
 | 
			
		||||
        final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
 | 
			
		||||
        final data = Uint8List.fromList(await file
 | 
			
		||||
            .openRead(beginCursor, endCursor)
 | 
			
		||||
            .expand((chunk) => chunk)
 | 
			
		||||
            .toList());
 | 
			
		||||
 | 
			
		||||
        final result = await _chunkedUploadOnePart(
 | 
			
		||||
          data,
 | 
			
		||||
@@ -253,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: () {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								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';
 | 
			
		||||
@@ -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(),
 | 
			
		||||
 
 | 
			
		||||
@@ -54,14 +54,20 @@ class AccountSettingsScreen extends StatelessWidget {
 | 
			
		||||
                child: DropdownButton2<Locale?>(
 | 
			
		||||
                  isExpanded: true,
 | 
			
		||||
                  items: [
 | 
			
		||||
                    ...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
 | 
			
		||||
                    ...EasyLocalization.of(context)!
 | 
			
		||||
                        .supportedLocales
 | 
			
		||||
                        .mapIndexed((idx, ele) {
 | 
			
		||||
                      return DropdownMenuItem<Locale?>(
 | 
			
		||||
                        value: Locale.parse(ele.toString()),
 | 
			
		||||
                        child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
 | 
			
		||||
                        child: Text('${ele.languageCode}-${ele.countryCode}')
 | 
			
		||||
                            .fontSize(14),
 | 
			
		||||
                      );
 | 
			
		||||
                    }),
 | 
			
		||||
                  ],
 | 
			
		||||
                  value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
 | 
			
		||||
                  value: ua.user?.language != null
 | 
			
		||||
                      ? (Locale.tryParse(ua.user!.language) ??
 | 
			
		||||
                          Locale.parse('en-US'))
 | 
			
		||||
                      : Locale.parse('en-US'),
 | 
			
		||||
                  onChanged: (Locale? value) {
 | 
			
		||||
                    if (value == null) return;
 | 
			
		||||
                    _setAccountLanguage(context, value);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:google_fonts/google_fonts.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
@@ -41,6 +42,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
  Future<void> _fetchWhatsNew() async {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    final resp = await sn.client.get('/cgi/im/whats-new');
 | 
			
		||||
    if (resp.data == null) return;
 | 
			
		||||
    final List<dynamic> out = resp.data;
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _unreadCounts = {for (var v in out) v['channel_id']: v['count']};
 | 
			
		||||
@@ -72,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);
 | 
			
		||||
    })
 | 
			
		||||
@@ -135,9 +139,30 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
    _fetchWhatsNew();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onTapChannel(SnChannel channel) {
 | 
			
		||||
    final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
 | 
			
		||||
 | 
			
		||||
    if (doExpand) {
 | 
			
		||||
      setState(() => _focusChannel = channel);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    GoRouter.of(context).pushNamed(
 | 
			
		||||
      'chatRoom',
 | 
			
		||||
      pathParameters: {
 | 
			
		||||
        'scope': channel.realm?.alias ?? 'global',
 | 
			
		||||
        'alias': channel.alias,
 | 
			
		||||
      },
 | 
			
		||||
    ).then((value) {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        _unreadCounts?[channel.id] = 0;
 | 
			
		||||
        setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
        _refreshChannels(noRemote: true);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
@@ -240,118 +265,17 @@ 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: () {
 | 
			
		||||
                          if (doExpand) {
 | 
			
		||||
                            setState(() => _focusChannel = channel);
 | 
			
		||||
                            return;
 | 
			
		||||
                          }
 | 
			
		||||
                          GoRouter.of(context).pushNamed(
 | 
			
		||||
                            'chatRoom',
 | 
			
		||||
                            pathParameters: {
 | 
			
		||||
                              'scope': channel.realm?.alias ?? 'global',
 | 
			
		||||
                              'alias': channel.alias,
 | 
			
		||||
                            },
 | 
			
		||||
                          ).then((value) {
 | 
			
		||||
                            if (mounted) {
 | 
			
		||||
                              _unreadCounts?[channel.id] = 0;
 | 
			
		||||
                              setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
                              _refreshChannels(noRemote: true);
 | 
			
		||||
                            }
 | 
			
		||||
                          });
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    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
 | 
			
		||||
                          ? 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: null,
 | 
			
		||||
                        fallbackWidget: const Icon(Symbols.chat, size: 20),
 | 
			
		||||
                      ),
 | 
			
		||||
                    return _ChatChannelEntry(
 | 
			
		||||
                      channel: channel,
 | 
			
		||||
                      lastMessage: lastMessage,
 | 
			
		||||
                      unreadCount: _unreadCounts?[channel.id],
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        if (doExpand) {
 | 
			
		||||
                          _unreadCounts?[channel.id] = 0;
 | 
			
		||||
                          setState(() => _focusChannel = channel);
 | 
			
		||||
                          return;
 | 
			
		||||
                        }
 | 
			
		||||
                        GoRouter.of(context).pushNamed(
 | 
			
		||||
                          'chatRoom',
 | 
			
		||||
                          pathParameters: {
 | 
			
		||||
                            'scope': channel.realm?.alias ?? 'global',
 | 
			
		||||
                            'alias': channel.alias,
 | 
			
		||||
                          },
 | 
			
		||||
                        ).then((value) {
 | 
			
		||||
                          if (mounted) {
 | 
			
		||||
                            setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
                            _refreshChannels(noRemote: true);
 | 
			
		||||
                          }
 | 
			
		||||
                        });
 | 
			
		||||
                        _onTapChannel(channel);
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
@@ -386,3 +310,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';
 | 
			
		||||
@@ -13,11 +14,13 @@ import 'package:surface/controllers/chat_message_controller.dart';
 | 
			
		||||
import 'package:surface/controllers/post_write_controller.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/chat_call.dart';
 | 
			
		||||
import 'package:surface/providers/notification.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
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';
 | 
			
		||||
@@ -57,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
 | 
			
		||||
@@ -84,6 +92,20 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
              orElse: () => null,
 | 
			
		||||
            );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _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);
 | 
			
		||||
@@ -209,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!);
 | 
			
		||||
@@ -232,6 +253,18 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _wsSubscription?.cancel();
 | 
			
		||||
    _messageController.dispose();
 | 
			
		||||
    _nty.skippableNotifyChannel = null;
 | 
			
		||||
    if (_channel != null) {
 | 
			
		||||
      _ws.conn?.sink.add(
 | 
			
		||||
        jsonEncode(WebSocketPackage(
 | 
			
		||||
          method: 'events.unsubscribe',
 | 
			
		||||
          endpoint: 'im',
 | 
			
		||||
          payload: {
 | 
			
		||||
            'channel_id': _channel!.id,
 | 
			
		||||
          },
 | 
			
		||||
        )),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -244,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)
 | 
			
		||||
@@ -282,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(
 | 
			
		||||
@@ -308,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,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import 'dart:math' as math;
 | 
			
		||||
import 'dart:ui';
 | 
			
		||||
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
@@ -94,8 +93,12 @@ class _HomeScreenState extends State<HomeScreen> {
 | 
			
		||||
                      : MainAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    _HomeDashUpdateWidget(
 | 
			
		||||
                        padding: const EdgeInsets.only(
 | 
			
		||||
                            bottom: 8, left: 8, right: 8)),
 | 
			
		||||
                      padding: const EdgeInsets.only(
 | 
			
		||||
                        bottom: 8,
 | 
			
		||||
                        left: 8,
 | 
			
		||||
                        right: 8,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    _HomeDashSpecialDayWidget().padding(horizontal: 8),
 | 
			
		||||
                    StaggeredGrid.extent(
 | 
			
		||||
                      maxCrossAxisExtent: 280,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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),
 | 
			
		||||
                          ],
 | 
			
		||||
 
 | 
			
		||||
@@ -95,8 +95,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 +126,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,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@@ -232,7 +237,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 ?? []);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -253,7 +259,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
              textAlign: TextAlign.center,
 | 
			
		||||
              text: TextSpan(children: [
 | 
			
		||||
                TextSpan(
 | 
			
		||||
                  text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
 | 
			
		||||
                  text: _writeController.title.isNotEmpty
 | 
			
		||||
                      ? _writeController.title
 | 
			
		||||
                      : 'untitled'.tr(),
 | 
			
		||||
                  style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
			
		||||
                        color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                      ),
 | 
			
		||||
@@ -280,7 +288,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
            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 +303,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 +326,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 +341,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 +356,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(
 | 
			
		||||
@@ -384,7 +399,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                      })
 | 
			
		||||
                          .padding(top: 8),
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
 | 
			
		||||
                    if (_writeController.attachments.isNotEmpty ||
 | 
			
		||||
                        _writeController.thumbnail != null)
 | 
			
		||||
                      Positioned(
 | 
			
		||||
                        bottom: 0,
 | 
			
		||||
                        left: 0,
 | 
			
		||||
@@ -393,16 +409,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 +434,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                              _writeController.setIsBusy(false);
 | 
			
		||||
                            }
 | 
			
		||||
                          },
 | 
			
		||||
                          onUpdateBusy: (state) => _writeController.setIsBusy(state),
 | 
			
		||||
                          onUpdateBusy: (state) =>
 | 
			
		||||
                              _writeController.setIsBusy(state),
 | 
			
		||||
                        ).padding(bottom: 8),
 | 
			
		||||
                      ),
 | 
			
		||||
                  ],
 | 
			
		||||
@@ -426,11 +446,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 +461,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 +477,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 +490,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 +513,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 +532,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 +560,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 +600,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 +613,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 +659,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 +672,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,7 +714,8 @@ 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) {
 | 
			
		||||
@@ -717,7 +767,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 +783,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 +802,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 +911,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 +949,8 @@ class _PostArticleEditor extends StatelessWidget {
 | 
			
		||||
              border: InputBorder.none,
 | 
			
		||||
            ),
 | 
			
		||||
            onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
            contentInsertionConfiguration: controller.contentInsertionConfiguration,
 | 
			
		||||
            contentInsertionConfiguration:
 | 
			
		||||
                controller.contentInsertionConfiguration,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
@@ -906,7 +963,8 @@ 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) {
 | 
			
		||||
@@ -958,7 +1016,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 +1028,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,8 +1044,10 @@ class _PostQuestionEditor extends StatelessWidget {
 | 
			
		||||
                    ),
 | 
			
		||||
                    border: InputBorder.none,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  contentInsertionConfiguration: controller.contentInsertionConfiguration,
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  contentInsertionConfiguration:
 | 
			
		||||
                      controller.contentInsertionConfiguration,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
@@ -1001,7 +1063,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 +1085,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 +1098,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 +1142,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 +1153,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.symmetric(horizontal: 12),
 | 
			
		||||
      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,8 +51,10 @@ 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(),
 | 
			
		||||
@@ -64,13 +66,20 @@ class _AppSharingListenerState extends State<AppSharingListener> {
 | 
			
		||||
                          },
 | 
			
		||||
                          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