Compare commits
67 Commits
2.3.2+72
...
92f7e92018
Author | SHA1 | Date | |
---|---|---|---|
92f7e92018 | |||
5c483bd3b8 | |||
1c510d63fe | |||
115cb4adc1 | |||
54c098c274 | |||
29731728cd | |||
9e8882c580 | |||
6042e57e7a | |||
6235e736b9 | |||
e075804782 | |||
d40a6ca1c4 | |||
5ac657e526 | |||
97ddc18b8e | |||
b835c8edea | |||
288c0399f9 | |||
1478933cf1 | |||
93c6fa6e53 | |||
ce6e9c185a | |||
cdaa8cfe58 | |||
76d8cd943d | |||
d6f3ffc655 | |||
5a6b841253 | |||
cb2de52bee | |||
64e2644745 | |||
56711889ab | |||
4f47cd2c0c | |||
2b61c372f5 | |||
73777fe74e | |||
33a4bd7e71 | |||
17e6b81f76 | |||
22fde6b400 | |||
6e03a00280 | |||
72e6a6a1f6 | |||
66aef44281 | |||
7bb73c80b0 | |||
d043ef2410 | |||
1d0e2f7591 | |||
e9ef28d764 | |||
289aa17a7a | |||
93f41bb523 | |||
09ec9d4a0c | |||
1153fbdeee | |||
e933058338 | |||
ae9743c84f | |||
32bf834108 | |||
1b41c847a6 | |||
b1af6c2c97 | |||
8e76ff3f84 | |||
bd26602299 | |||
52ab1d0d10 | |||
f746e06f65 | |||
d11069a2be | |||
d6dc487d9e | |||
a07c7cdede | |||
acbc125dec | |||
ad0ee971c1 | |||
52d6bb083e | |||
2027eab49b | |||
566ebde1dd | |||
9e039cc532 | |||
c4b95d7084 | |||
a66129a9ba | |||
44e1a8bf67 | |||
efcfd3f57d | |||
84759715a4 | |||
fda09382dd | |||
2c5dd0563a |
11
api/Nexus/Check Status.bru
Normal file
11
api/Nexus/Check Status.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Check Status
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/directory/status
|
||||
body: none
|
||||
auth: none
|
||||
}
|
11
api/Nexus/List Services.bru
Normal file
11
api/Nexus/List Services.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List Services
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/directory/services
|
||||
body: none
|
||||
auth: none
|
||||
}
|
18
api/Passport/Deal Abuse Report.bru
Normal file
18
api/Passport/Deal Abuse Report.bru
Normal file
@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: Deal Abuse Report
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{endpoint}}/cgi/id/reports/abuse/3/status
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"status": "processed",
|
||||
"message": "相关附件已经进行评级处理,未来会将该项权限下放到帖主以及社区成员。"
|
||||
}
|
||||
}
|
BIN
assets/fonts/Nunito-Bold.ttf
Executable file
BIN
assets/fonts/Nunito-Bold.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/Nunito-Italic.ttf
Executable file
BIN
assets/fonts/Nunito-Italic.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/Nunito-Regular.ttf
Executable file
BIN
assets/fonts/Nunito-Regular.ttf
Executable file
Binary file not shown.
@ -153,6 +153,11 @@
|
||||
"publisherRunBy": "Run by {}",
|
||||
"fieldPublisherBelongToRealm": "Belongs to",
|
||||
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
||||
"writePost": "Compose",
|
||||
"postTypeStory": "Story",
|
||||
"postTypeArticle": "Article",
|
||||
"postTypeQuestion": "Question",
|
||||
"postTypeVideo": "Video",
|
||||
"writePostTypeStory": "Post a story",
|
||||
"writePostTypeArticle": "Write an article",
|
||||
"writePostTypeQuestion": "Ask a question",
|
||||
@ -203,6 +208,11 @@
|
||||
"other": "{} comments"
|
||||
},
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsCustomFonts": "Custom Fonts",
|
||||
"settingsCustomFontsDescription": "Set custom fonts for the application.",
|
||||
"settingsCustomFontFamily": "Custom Font Family",
|
||||
"settingsCustomFontFamilyHint": "Use comma to separate fonts, higher priority comes first",
|
||||
"settingsCustomFontApplied": "Custom font has been applied.",
|
||||
"settingsDisplayLanguage": "Display Language",
|
||||
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||
"settingsDisplayLanguageSystem": "Follow System",
|
||||
@ -512,8 +522,13 @@
|
||||
"accountBirthday": "Born on {}",
|
||||
"accountBadge": "Badge",
|
||||
"accountCheckInNoRecords": "No check-in records",
|
||||
"badgeCompanyStaff": "Solsynth Staff",
|
||||
"badgeCompanyStaff": "Staff",
|
||||
"badgeSiteMigration": "Solar Network Native",
|
||||
"badgeCommunitySurvey": "Survey Participant",
|
||||
"badgeCommunityVerified": "Verified User",
|
||||
"badgeCommunityContributor": "Great Contributor",
|
||||
"badgeSiteAnniversary": "Anniversary",
|
||||
"badgeUserBirthday": "Birthday",
|
||||
"accountStatus": "Status",
|
||||
"accountStatusOnline": "Online",
|
||||
"accountStatusOffline": "Offline",
|
||||
@ -719,7 +734,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"
|
||||
}
|
||||
|
@ -137,6 +137,11 @@
|
||||
"publisherRunBy": "由 {} 管理",
|
||||
"fieldPublisherBelongToRealm": "所属领域",
|
||||
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
||||
"writePost": "撰写",
|
||||
"postTypeStory": "动态",
|
||||
"postTypeArticle": "文章",
|
||||
"postTypeQuestion": "问题",
|
||||
"postTypeVideo": "视频",
|
||||
"writePostTypeStory": "发动态",
|
||||
"writePostTypeArticle": "写文章",
|
||||
"writePostTypeQuestion": "提问题",
|
||||
@ -201,6 +206,11 @@
|
||||
"other": "{} 条评论"
|
||||
},
|
||||
"settingsAppearance": "外观",
|
||||
"settingsCustomFonts": "自定义字体",
|
||||
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
|
||||
"settingsCustomFontFamily": "应用字体",
|
||||
"settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高",
|
||||
"settingsCustomFontApplied": "自定义字体已经应用。",
|
||||
"settingsDisplayLanguage": "显示语言",
|
||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||
"settingsDisplayLanguageSystem": "跟随系统",
|
||||
@ -510,8 +520,13 @@
|
||||
"accountBirthday": "出生于 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暂无运势记录",
|
||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||
"badgeCompanyStaff": "工作人员",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"badgeCommunitySurvey": "调研参与者",
|
||||
"badgeCommunityVerified": "认证用户",
|
||||
"badgeCommunityContributor": "优秀社区贡献者",
|
||||
"badgeSiteAnniversary": "周年纪念",
|
||||
"badgeUserBirthday": "生日纪念",
|
||||
"accountStatus": "状态",
|
||||
"accountStatusOnline": "在线",
|
||||
"accountStatusOffline": "离线",
|
||||
@ -717,7 +732,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": "無法預覽加密消息"
|
||||
}
|
||||
|
@ -4,4 +4,8 @@ targets:
|
||||
json_serializable:
|
||||
options:
|
||||
explicit_to_json: true
|
||||
field_rename: snake
|
||||
field_rename: snake
|
||||
drift_dev:
|
||||
options:
|
||||
databases:
|
||||
my_database: lib/database/database.dart
|
1
drift_schemas/my_database/drift_schema_v1.json
Normal file
1
drift_schemas/my_database/drift_schema_v1.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]}
|
1
drift_schemas/my_database/drift_schema_v2.json
Normal file
1
drift_schemas/my_database/drift_schema_v2.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}}]}
|
1
drift_schemas/my_database/drift_schema_v3.json
Normal file
1
drift_schemas/my_database/drift_schema_v3.json
Normal file
File diff suppressed because one or more lines are too long
@ -37,6 +37,8 @@ PODS:
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- fast_rsa (0.6.0):
|
||||
- Flutter
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
@ -52,14 +54,14 @@ PODS:
|
||||
- Firebase/Messaging (11.8.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.8.0)
|
||||
- firebase_analytics (11.4.3):
|
||||
- firebase_analytics (11.4.4):
|
||||
- Firebase/Analytics (= 11.8.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.12.0):
|
||||
- firebase_core (3.12.1):
|
||||
- Firebase/CoreOnly (= 11.8.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.2.3):
|
||||
- firebase_messaging (15.2.4):
|
||||
- Firebase/Messaging (= 11.8.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
@ -113,6 +115,8 @@ PODS:
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_timezone (0.0.1):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
@ -122,6 +126,8 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- GoogleAppMeasurement (11.8.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -235,7 +241,7 @@ PODS:
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.49.0)
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
@ -258,6 +264,7 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- fast_rsa (from `.symlinks/plugins/fast_rsa/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||
@ -267,9 +274,11 @@ DEPENDENCIES:
|
||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
@ -325,6 +334,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/croppy/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
fast_rsa:
|
||||
:path: ".symlinks/plugins/fast_rsa/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
file_saver:
|
||||
@ -343,12 +354,16 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_timezone:
|
||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||
flutter_udid:
|
||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||
flutter_webrtc:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/darwin"
|
||||
geolocator_apple:
|
||||
:path: ".symlinks/plugins/geolocator_apple/ios"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
@ -401,12 +416,13 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
fast_rsa: dc48fb05f26bb108863de122b2a9f5554e8e2591
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_analytics: 7ec1166af61987fa968766eb11587c562a5650ee
|
||||
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
|
||||
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
|
||||
firebase_analytics: e3b6782e70e32b7fa18f7cd233e3201975dd86aa
|
||||
firebase_core: ac395f994af4e28f6a38b59e05a88ca57abeb874
|
||||
firebase_messaging: 7e223f4ee7ca053bf4ce43748e84a6d774ec9728
|
||||
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
@ -416,9 +432,11 @@ SPEC CHECKSUMS:
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1
|
||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
@ -445,7 +463,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
@ -8,7 +7,10 @@ import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/database/database.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/keypair.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
@ -25,6 +27,8 @@ class ChatMessageController extends ChangeNotifier {
|
||||
late final WebSocketProvider _ws;
|
||||
late final SnAttachmentProvider _attach;
|
||||
late final DatabaseProvider _dt;
|
||||
late final ChatChannelProvider _ct;
|
||||
late final KeyPairProvider _kp;
|
||||
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
@ -33,11 +37,14 @@ class ChatMessageController extends ChangeNotifier {
|
||||
_ud = context.read<UserDirectoryProvider>();
|
||||
_ws = context.read<WebSocketProvider>();
|
||||
_attach = context.read<SnAttachmentProvider>();
|
||||
_ct = context.read<ChatChannelProvider>();
|
||||
_dt = context.read<DatabaseProvider>();
|
||||
_kp = context.read<KeyPairProvider>();
|
||||
}
|
||||
|
||||
bool isPending = true;
|
||||
bool isLoading = false;
|
||||
bool isAggressiveLoading = false;
|
||||
|
||||
int? messageTotal;
|
||||
|
||||
@ -61,10 +68,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
channel = chan;
|
||||
|
||||
// Fetch channel profile
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/im/channels/${chan.keyPath}/me',
|
||||
);
|
||||
profile = SnChannelMember.fromJson(resp.data);
|
||||
profile = await _ct.getChannelProfile(channel!);
|
||||
|
||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
@ -183,6 +187,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
} else {
|
||||
messages.insert(0, message);
|
||||
}
|
||||
notifyListeners();
|
||||
await _applyMessage(message);
|
||||
notifyListeners();
|
||||
|
||||
@ -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,10 +346,11 @@ 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()
|
||||
..where((e) => e.channelId.equals(channel!.id))
|
||||
..limit(1)
|
||||
..orderBy([
|
||||
(e) =>
|
||||
@ -331,6 +360,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
if (mostRecentMessage == null) {
|
||||
// Initial load
|
||||
await loadMessages(take: 20);
|
||||
isAggressiveLoading = false;
|
||||
isCheckedUpdate = true;
|
||||
return;
|
||||
}
|
||||
@ -348,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((_) {
|
||||
@ -529,7 +565,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
},
|
||||
).toJson(),
|
||||
));
|
||||
log('[Messaging] Send read event request: $_readEventAnchor');
|
||||
logging.debug('[Messaging] Send read event request: $_readEventAnchor');
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -71,7 +71,8 @@ class PostWriteMedia {
|
||||
}
|
||||
}
|
||||
|
||||
PostWriteMedia.fromBytes(this.raw, this.name, this.type, {this.attachment, this.file});
|
||||
PostWriteMedia.fromBytes(this.raw, this.name, this.type,
|
||||
{this.attachment, this.file});
|
||||
|
||||
bool get isEmpty => attachment == null && file == null && raw == null;
|
||||
|
||||
@ -105,7 +106,8 @@ class PostWriteMedia {
|
||||
}) {
|
||||
if (attachment != null) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||
final ImageProvider provider =
|
||||
UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||
if (width != null && height != null && !kIsWeb) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
@ -116,7 +118,8 @@ class PostWriteMedia {
|
||||
}
|
||||
return provider;
|
||||
} else if (file != null) {
|
||||
final ImageProvider provider = kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
||||
final ImageProvider provider =
|
||||
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
||||
if (width != null && height != null) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
@ -159,11 +162,14 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
final TextEditingController rewardController = TextEditingController();
|
||||
|
||||
ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration(
|
||||
ContentInsertionConfiguration get contentInsertionConfiguration =>
|
||||
ContentInsertionConfiguration(
|
||||
onContentInserted: (KeyboardInsertedContent content) {
|
||||
if (content.hasData) {
|
||||
addAttachments(
|
||||
[PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]);
|
||||
addAttachments([
|
||||
PostWriteMedia.fromBytes(content.data!,
|
||||
'attachmentInsertedImage'.tr(), SnMediaType.image)
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -193,7 +199,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
String get description => descriptionController.text;
|
||||
|
||||
bool get isRelatedNull => ![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
|
||||
bool get isRelatedNull =>
|
||||
![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
|
||||
|
||||
bool isLoading = false, isBusy = false;
|
||||
double? progress;
|
||||
@ -237,14 +244,18 @@ class PostWriteController extends ChangeNotifier {
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||
invisibleUsers =
|
||||
List.from(post.invisibleUsersList ?? [], growable: true);
|
||||
visibility = post.visibility;
|
||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
categories =
|
||||
List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(
|
||||
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
poll = post.preload?.poll;
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
if (post.preload?.thumbnail != null &&
|
||||
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||
}
|
||||
if (post.preload?.realm != null) {
|
||||
@ -272,7 +283,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
|
||||
Future<SnAttachment> _uploadAttachment(
|
||||
BuildContext context, PostWriteMedia media,
|
||||
{bool isCompressed = false}) async {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
|
||||
@ -281,7 +293,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
media.name,
|
||||
'interactive',
|
||||
null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image
|
||||
? 'image/png'
|
||||
: null,
|
||||
);
|
||||
|
||||
var item = await attach.chunkedUploadParts(
|
||||
@ -297,9 +311,11 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
||||
try {
|
||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||
final compressedAttachment =
|
||||
await _tryCompressVideoCopy(context, media);
|
||||
if (compressedAttachment != null) {
|
||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||
item = await attach.updateOne(item,
|
||||
compressedId: compressedAttachment.id);
|
||||
}
|
||||
} catch (err) {
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
@ -309,8 +325,10 @@ class PostWriteController extends ChangeNotifier {
|
||||
return item;
|
||||
}
|
||||
|
||||
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
|
||||
Future<SnAttachment?> _tryCompressVideoCopy(
|
||||
BuildContext context, PostWriteMedia media) async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS))
|
||||
return null;
|
||||
if (media.type != SnMediaType.video) return null;
|
||||
if (media.file == null) return null;
|
||||
if (VideoCompress.isCompressing) return null;
|
||||
@ -334,7 +352,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (!context.mounted) return null;
|
||||
|
||||
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
|
||||
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
|
||||
final compressedAttachment =
|
||||
await _uploadAttachment(context, compressedMedia, isCompressed: true);
|
||||
|
||||
return compressedAttachment;
|
||||
}
|
||||
@ -370,18 +389,25 @@ class PostWriteController extends ChangeNotifier {
|
||||
'content': contentController.text,
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (descriptionController.text.isNotEmpty)
|
||||
'description': descriptionController.text,
|
||||
if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||
'attachments':
|
||||
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
||||
if (thumbnail != null && thumbnail!.attachment != null)
|
||||
'thumbnail': thumbnail!.attachment!.toJson(),
|
||||
'attachments': attachments
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.toJson())
|
||||
.toList(growable: true),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'categories':
|
||||
categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedAt != null)
|
||||
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null)
|
||||
'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
if (poll != null) 'poll': poll!.toJson(),
|
||||
@ -391,6 +417,12 @@ class PostWriteController extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
bool get isNotEmpty =>
|
||||
title.isNotEmpty ||
|
||||
description.isNotEmpty ||
|
||||
contentController.text.isNotEmpty ||
|
||||
attachments.isNotEmpty;
|
||||
|
||||
bool temporaryRestored = false;
|
||||
|
||||
void _temporaryLoad() {
|
||||
@ -403,18 +435,24 @@ class PostWriteController extends ChangeNotifier {
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
rewardController.text = data['reward']?.toString() ?? '';
|
||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments
|
||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||
if (data['thumbnail'] != null)
|
||||
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments.addAll(data['attachments']
|
||||
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
|
||||
.cast<PostWriteMedia>());
|
||||
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||
visibility = data['visibility'];
|
||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
if (data['published_at'] != null)
|
||||
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null)
|
||||
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost =
|
||||
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost =
|
||||
data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
|
||||
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
|
||||
temporaryRestored = true;
|
||||
@ -463,7 +501,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
media.name,
|
||||
'interactive',
|
||||
null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image
|
||||
? 'image/png'
|
||||
: null,
|
||||
);
|
||||
|
||||
var item = await attach.chunkedUploadParts(
|
||||
@ -472,16 +512,20 @@ class PostWriteController extends ChangeNotifier {
|
||||
place.$2,
|
||||
onProgress: (value) {
|
||||
// Calculate overall progress for attachments
|
||||
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
|
||||
progress = math.max(
|
||||
((i + value) / attachments.length) * kAttachmentProgressWeight,
|
||||
value);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
if (context.mounted) {
|
||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||
final compressedAttachment =
|
||||
await _tryCompressVideoCopy(context, media);
|
||||
if (compressedAttachment != null) {
|
||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||
item = await attach.updateOne(item,
|
||||
compressedId: compressedAttachment.id);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@ -518,16 +562,23 @@ class PostWriteController extends ChangeNotifier {
|
||||
'content': contentController.text,
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
|
||||
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||
if (descriptionController.text.isNotEmpty)
|
||||
'description': descriptionController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null)
|
||||
'thumbnail': thumbnail!.attachment!.rid,
|
||||
'attachments': attachments
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.rid)
|
||||
.toList(),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedAt != null)
|
||||
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null)
|
||||
'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||
if (reward != null) 'reward': reward,
|
||||
@ -536,11 +587,14 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (realm != null) 'realm': realm!.id,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
progress =
|
||||
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
notifyListeners();
|
||||
},
|
||||
onReceiveProgress: (count, total) {
|
||||
progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2);
|
||||
progress = baseProgressVal +
|
||||
(kPostingProgressWeight / 2) +
|
||||
(count / total) * (kPostingProgressWeight / 2);
|
||||
notifyListeners();
|
||||
},
|
||||
options: Options(
|
||||
@ -683,7 +737,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
temporaryRestored = false;
|
||||
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
SharedPreferences.getInstance()
|
||||
.then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
42
lib/database/account.dart
Normal file
42
lib/database/account.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
|
||||
class SnAccountConverter extends TypeConverter<SnAccount, String>
|
||||
with JsonTypeConverter2<SnAccount, String, Map<String, Object?>> {
|
||||
const SnAccountConverter();
|
||||
|
||||
@override
|
||||
SnAccount fromSql(String fromDb) {
|
||||
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SnAccount value) {
|
||||
return jsonEncode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
SnAccount fromJson(Map<String, Object?> json) {
|
||||
return SnAccount.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(SnAccount value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@TableIndex(name: 'idx_account_name', columns: {#name})
|
||||
class SnLocalAccount extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get name => text()();
|
||||
|
||||
TextColumn get content => text().map(const SnAccountConverter())();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||
}
|
47
lib/database/attachment.dart
Normal file
47
lib/database/attachment.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
|
||||
class SnAttachmentConverter extends TypeConverter<SnAttachment, String>
|
||||
with JsonTypeConverter2<SnAttachment, String, Map<String, Object?>> {
|
||||
const SnAttachmentConverter();
|
||||
|
||||
@override
|
||||
SnAttachment fromSql(String fromDb) {
|
||||
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SnAttachment value) {
|
||||
return jsonEncode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
SnAttachment fromJson(Map<String, Object?> json) {
|
||||
return SnAttachment.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(SnAttachment value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@TableIndex(name: 'idx_attachment_rid', columns: {#rid})
|
||||
@TableIndex(name: 'idx_attachment_account', columns: {#accountId})
|
||||
class SnLocalAttachment extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get rid => text().unique()();
|
||||
|
||||
TextColumn get uuid => text().unique()();
|
||||
|
||||
TextColumn get content => text().map(const SnAttachmentConverter())();
|
||||
|
||||
IntColumn get accountId => integer()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||
}
|
@ -28,6 +28,7 @@ class SnChannelConverter extends TypeConverter<SnChannel, String>
|
||||
}
|
||||
}
|
||||
|
||||
@TableIndex(name: 'idx_channel_alias', columns: {#alias})
|
||||
class SnLocalChatChannel extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
@ -63,12 +64,54 @@ class SnMessageConverter extends TypeConverter<SnChatMessage, String>
|
||||
}
|
||||
}
|
||||
|
||||
@TableIndex(name: 'idx_chat_channel', columns: {#channelId})
|
||||
class SnLocalChatMessage extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
IntColumn get channelId => integer()();
|
||||
|
||||
IntColumn get senderId => integer().nullable()();
|
||||
|
||||
TextColumn get content => text().map(const SnMessageConverter())();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
class SnChannelMemberConverter extends TypeConverter<SnChannelMember, String>
|
||||
with JsonTypeConverter2<SnChannelMember, String, Map<String, Object?>> {
|
||||
const SnChannelMemberConverter();
|
||||
|
||||
@override
|
||||
SnChannelMember fromSql(String fromDb) {
|
||||
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SnChannelMember value) {
|
||||
return jsonEncode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
SnChannelMember fromJson(Map<String, Object?> json) {
|
||||
return SnChannelMember.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(SnChannelMember value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
class SnLocalChannelMember extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
IntColumn get channelId => integer()();
|
||||
|
||||
IntColumn get accountId => integer()();
|
||||
|
||||
TextColumn get content => text().map(SnChannelMemberConverter())();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||
}
|
||||
|
@ -1,17 +1,33 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:surface/database/account.dart';
|
||||
import 'package:surface/database/attachment.dart';
|
||||
import 'package:surface/database/chat.dart';
|
||||
import 'package:surface/database/database.steps.dart';
|
||||
import 'package:surface/database/keypair.dart';
|
||||
import 'package:surface/database/sticker.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [SnLocalChatChannel, SnLocalChatMessage])
|
||||
@DriftDatabase(tables: [
|
||||
SnLocalChatChannel,
|
||||
SnLocalChatMessage,
|
||||
SnLocalChannelMember,
|
||||
SnLocalKeyPair,
|
||||
SnLocalAccount,
|
||||
SnLocalAttachment,
|
||||
SnLocalSticker,
|
||||
SnLocalStickerPack,
|
||||
])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 3;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
@ -25,4 +41,15 @@ class AppDatabase extends _$AppDatabase {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onUpgrade: stepByStep(from1To2: (m, schema) async {
|
||||
// Nothing else to do here
|
||||
}, from2To3: (m, schema) async {
|
||||
// Nothing else to do here, too
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
445
lib/database/database.steps.dart
Normal file
445
lib/database/database.steps.dart
Normal file
@ -0,0 +1,445 @@
|
||||
// dart format width=80
|
||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||
import 'package:drift/drift.dart' as i1;
|
||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
final class Schema2 extends i0.VersionedSchema {
|
||||
Schema2({required super.database}) : super(version: 2);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
snLocalChatChannel,
|
||||
snLocalChatMessage,
|
||||
snLocalKeyPair,
|
||||
];
|
||||
late final Shape0 snLocalChatChannel = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_channel',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape1 snLocalChatMessage = Shape1(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_message',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape2 snLocalKeyPair = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_key_pair',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
}
|
||||
|
||||
class Shape0 extends i0.VersionedTable {
|
||||
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get alias =>
|
||||
columnsByName['alias']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: i1.DriftSqlType.int,
|
||||
defaultConstraints:
|
||||
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('alias', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('content', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<DateTime> _column_3(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
defaultValue: const CustomExpression(
|
||||
'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)'));
|
||||
|
||||
class Shape1 extends i0.VersionedTable {
|
||||
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get channelId =>
|
||||
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_4(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('channel_id', aliasedName, false,
|
||||
type: i1.DriftSqlType.int);
|
||||
|
||||
class Shape2 extends i0.VersionedTable {
|
||||
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get accountId =>
|
||||
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get publicKey =>
|
||||
columnsByName['public_key']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get privateKey =>
|
||||
columnsByName['private_key']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isActive =>
|
||||
columnsByName['is_active']! as i1.GeneratedColumn<bool>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_5(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('id', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('account_id', aliasedName, false,
|
||||
type: i1.DriftSqlType.int);
|
||||
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('public_key', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('private_key', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<bool> _column_9(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>('is_active', aliasedName, false,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_active" IN (0, 1))'),
|
||||
defaultValue: const CustomExpression('0'));
|
||||
|
||||
final class Schema3 extends i0.VersionedSchema {
|
||||
Schema3({required super.database}) : super(version: 3);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
snLocalChatChannel,
|
||||
snLocalChatMessage,
|
||||
snLocalChannelMember,
|
||||
snLocalKeyPair,
|
||||
snLocalAccount,
|
||||
snLocalAttachment,
|
||||
snLocalSticker,
|
||||
snLocalStickerPack,
|
||||
idxChannelAlias,
|
||||
idxChatChannel,
|
||||
idxAccountName,
|
||||
idxAttachmentRid,
|
||||
idxAttachmentAccount,
|
||||
];
|
||||
late final Shape0 snLocalChatChannel = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_channel',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape3 snLocalChatMessage = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_message',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_10,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape4 snLocalChannelMember = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_channel_member',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_6,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape2 snLocalKeyPair = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_key_pair',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape5 snLocalAccount = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_account',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_12,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape6 snLocalAttachment = Shape6(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_attachment',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_2,
|
||||
_column_6,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape7 snLocalSticker = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_sticker',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_15,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape8 snLocalStickerPack = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_sticker_pack',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
|
||||
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
|
||||
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
|
||||
final i1.Index idxAccountName = i1.Index('idx_account_name',
|
||||
'CREATE INDEX idx_account_name ON sn_local_account (name)');
|
||||
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
|
||||
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
|
||||
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||
}
|
||||
|
||||
class Shape3 extends i0.VersionedTable {
|
||||
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get channelId =>
|
||||
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get senderId =>
|
||||
columnsByName['sender_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_10(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('sender_id', aliasedName, true,
|
||||
type: i1.DriftSqlType.int);
|
||||
|
||||
class Shape4 extends i0.VersionedTable {
|
||||
Shape4({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get channelId =>
|
||||
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get accountId =>
|
||||
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
|
||||
type: i1.DriftSqlType.dateTime);
|
||||
|
||||
class Shape5 extends i0.VersionedTable {
|
||||
Shape5({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('name', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
|
||||
class Shape6 extends i0.VersionedTable {
|
||||
Shape6({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get rid =>
|
||||
columnsByName['rid']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get uuid =>
|
||||
columnsByName['uuid']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get accountId =>
|
||||
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('rid', aliasedName, false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('uuid', aliasedName, false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||
|
||||
class Shape7 extends i0.VersionedTable {
|
||||
Shape7({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get alias =>
|
||||
columnsByName['alias']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get fullAlias =>
|
||||
columnsByName['full_alias']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('full_alias', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
|
||||
class Shape8 extends i0.VersionedTable {
|
||||
Shape8({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
case 1:
|
||||
final schema = Schema2(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from1To2(migrator, schema);
|
||||
return 2;
|
||||
case 2:
|
||||
final schema = Schema3(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
));
|
16
lib/database/keypair.dart
Normal file
16
lib/database/keypair.dart
Normal file
@ -0,0 +1,16 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class SnLocalKeyPair extends Table {
|
||||
TextColumn get id => text()();
|
||||
|
||||
IntColumn get accountId => integer()();
|
||||
|
||||
TextColumn get publicKey => text()();
|
||||
|
||||
TextColumn get privateKey => text().nullable()();
|
||||
|
||||
BoolColumn get isActive => boolean().withDefault(Constant(false))();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
74
lib/database/sticker.dart
Normal file
74
lib/database/sticker.dart
Normal file
@ -0,0 +1,74 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
|
||||
class SnStickerConverter extends TypeConverter<SnSticker, String>
|
||||
with JsonTypeConverter2<SnSticker, String, Map<String, Object?>> {
|
||||
const SnStickerConverter();
|
||||
|
||||
@override
|
||||
SnSticker fromSql(String fromDb) {
|
||||
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SnSticker value) {
|
||||
return jsonEncode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
SnSticker fromJson(Map<String, Object?> json) {
|
||||
return SnSticker.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(SnSticker value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
class SnLocalSticker extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get alias => text()();
|
||||
|
||||
TextColumn get fullAlias => text()();
|
||||
|
||||
TextColumn get content => text().map(const SnStickerConverter())();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
class SnStickerPackConverter extends TypeConverter<SnStickerPack, String>
|
||||
with JsonTypeConverter2<SnStickerPack, String, Map<String, Object?>> {
|
||||
const SnStickerPackConverter();
|
||||
|
||||
@override
|
||||
SnStickerPack fromSql(String fromDb) {
|
||||
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SnStickerPack value) {
|
||||
return jsonEncode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
SnStickerPack fromJson(Map<String, Object?> json) {
|
||||
return SnStickerPack.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(SnStickerPack value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
class SnLocalStickerPack extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get content => text().map(const SnStickerPackConverter())();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
10
lib/logger.dart
Normal file
10
lib/logger.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:talker/talker.dart';
|
||||
|
||||
final logging = Talker(
|
||||
settings: TalkerSettings(
|
||||
enabled: true,
|
||||
useHistory: true,
|
||||
maxHistoryItems: 1000,
|
||||
useConsoleLogs: true,
|
||||
),
|
||||
);
|
@ -20,10 +20,12 @@ import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/firebase_options.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/keypair.dart';
|
||||
import 'package:surface/providers/link_preview.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
@ -160,6 +162,7 @@ class SolianApp extends StatelessWidget {
|
||||
Provider(create: (ctx) => SnStickerProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
||||
Provider(create: (ctx) => KeyPairProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
||||
@ -235,7 +238,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
await inAppReview.requestReview();
|
||||
prefs.setBool('rating_requested', true);
|
||||
} else {
|
||||
log('Unable request app review, unavailable');
|
||||
logging.error('Unable request app review, unavailable');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -263,17 +266,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
||||
final localBuildNumber =
|
||||
int.tryParse(localVersionString.split('+').last) ?? 0;
|
||||
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
||||
logging.info(
|
||||
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
||||
if ((remoteVersion > localVersion ||
|
||||
remoteBuildNumber > localBuildNumber) &&
|
||||
mounted) {
|
||||
final config = context.read<ConfigProvider>();
|
||||
config.setUpdate(
|
||||
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
||||
log("[Update] Update available: $remoteVersionString");
|
||||
logging.info("[Update] Update available: $remoteVersionString");
|
||||
}
|
||||
} catch (e) {
|
||||
log('[Error] Unable to check update: $e');
|
||||
logging.error('[Error] Unable to check update...', e);
|
||||
if (mounted) context.showErrorDialog('Unable to check update: $e');
|
||||
}
|
||||
}
|
||||
@ -304,9 +308,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
if (!mounted) return;
|
||||
final kp = context.read<KeyPairProvider>();
|
||||
await kp.reloadActive();
|
||||
kp.listen();
|
||||
if (!mounted) return;
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listSticker();
|
||||
log('[Bootstrap] Everything initialized!');
|
||||
if (!mounted) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final userCacheSize = await ud.loadAccountCache();
|
||||
logging.info('[Users] Loaded local user cache, size: $userCacheSize');
|
||||
logging.info('[Bootstrap] Everything initialized!');
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
@ -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',
|
||||
@ -78,10 +80,25 @@ class NotificationProvider extends ChangeNotifier {
|
||||
int showingTrayCount = 0;
|
||||
List<SnNotification> notifications = List.empty(growable: true);
|
||||
|
||||
int? skippableNotifyChannel;
|
||||
bool isMuted = false;
|
||||
|
||||
void listen() {
|
||||
_ws.pk.stream.listen((event) {
|
||||
if (event.method == 'notifications.new') {
|
||||
final notification = SnNotification.fromJson(event.payload!);
|
||||
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.mediumImpact();
|
||||
|
||||
if (notification.topic == 'messaging.message' &&
|
||||
skippableNotifyChannel != null) {
|
||||
if (notification.metadata['channel_id'] != null &&
|
||||
notification.metadata['channel_id'] == skippableNotifyChannel) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (showingCount < 0) showingCount = 0;
|
||||
showingCount++;
|
||||
showingTrayCount++;
|
||||
@ -92,10 +109,8 @@ class NotificationProvider extends ChangeNotifier {
|
||||
});
|
||||
notifyListeners();
|
||||
updateTray();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.mediumImpact();
|
||||
|
||||
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: () {
|
||||
|
117
lib/router.dart
117
lib/router.dart
@ -4,7 +4,9 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/screens/account.dart';
|
||||
import 'package:surface/screens/account/account_settings.dart';
|
||||
import 'package:surface/screens/account/badges.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/screens/account/keypairs.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||
@ -21,6 +23,7 @@ import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/screens/explore.dart';
|
||||
import 'package:surface/screens/friend.dart';
|
||||
import 'package:surface/screens/home.dart';
|
||||
import 'package:surface/screens/logging.dart';
|
||||
import 'package:surface/screens/news/news_detail.dart';
|
||||
import 'package:surface/screens/news/news_list.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
@ -63,10 +66,10 @@ final _appRoutes = [
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/write/:mode',
|
||||
path: '/write',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) => PostEditorScreen(
|
||||
mode: state.pathParameters['mode']!,
|
||||
mode: state.uri.queryParameters['mode'],
|
||||
postEditId: int.tryParse(
|
||||
state.uri.queryParameters['editing'] ?? '',
|
||||
),
|
||||
@ -105,55 +108,66 @@ final _appRoutes = [
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/wallet',
|
||||
name: 'accountWallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/badges',
|
||||
name: 'accountBadges',
|
||||
builder: (context, state) => const AccountBadgesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/wallet',
|
||||
name: 'accountWallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/keypairs',
|
||||
name: 'accountKeyPairs',
|
||||
builder: (context, state) => const KeyPairScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
name: 'factorSettings',
|
||||
builder: (context, state) => FactorSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
name: 'factorSettings',
|
||||
builder: (context, state) => FactorSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
@ -249,6 +263,11 @@ final _appRoutes = [
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/debug/logging',
|
||||
name: 'debugLogging',
|
||||
builder: (context, state) => const DebugLoggingScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
|
@ -125,8 +125,14 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
),
|
||||
Text(ua.user!.description)
|
||||
.textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
Text(
|
||||
(ua.user!.profile?.description.isNotEmpty ?? false)
|
||||
? ua.user!.profile!.description
|
||||
: 'userNoDescription'.tr(),
|
||||
style: (ua.user!.profile?.description.isEmpty ?? true)
|
||||
? TextStyle(fontStyle: FontStyle.italic)
|
||||
: null,
|
||||
).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -172,6 +178,26 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('accountWallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountBadges').tr(),
|
||||
subtitle: Text('accountBadgesDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.award_star),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountBadges');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountKeyPairs').tr(),
|
||||
subtitle: Text('accountKeyPairsDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.key),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountKeyPairs');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettings').tr(),
|
||||
subtitle: Text('accountSettingsSubtitle').tr(),
|
||||
|
@ -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) {
|
||||
@ -179,7 +204,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||
child: const Icon(Symbols.close, size: 28),
|
||||
@ -188,7 +212,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
children: [
|
||||
Row(
|
||||
@ -240,108 +263,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)
|
||||
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) _refreshChannels(noRemote: true);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(channel.name)),
|
||||
const Gap(8),
|
||||
if (_unreadCounts?[channel.id] != null)
|
||||
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 (value == true) _refreshChannels(noRemote: true);
|
||||
});
|
||||
_onTapChannel(channel);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -376,3 +308,100 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return chatList;
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatChannelEntry extends StatelessWidget {
|
||||
final SnChannel channel;
|
||||
final int? unreadCount;
|
||||
final SnChatMessage? lastMessage;
|
||||
final Function? onTap;
|
||||
const _ChatChannelEntry({
|
||||
required this.channel,
|
||||
this.unreadCount,
|
||||
this.lastMessage,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
final otherMember = channel.type == 1
|
||||
? channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
orElse: () => null,
|
||||
)
|
||||
: null;
|
||||
|
||||
final title = otherMember != null
|
||||
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
|
||||
: channel.name;
|
||||
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(title)),
|
||||
const Gap(8),
|
||||
if (unreadCount != null && unreadCount! > 0)
|
||||
Badge(
|
||||
label: Text(unreadCount.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: lastMessage != null
|
||||
? Row(
|
||||
children: [
|
||||
Badge(
|
||||
label: Text(
|
||||
ud.getFromCache(lastMessage!.sender.accountId)?.nick ??
|
||||
'unknown'.tr()),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
const Gap(6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
lastMessage!.body['algorithm'] == 'plain'
|
||||
? lastMessage!.body['text'] ??
|
||||
'messageUnablePreview'.tr()
|
||||
: 'messageUnablePreviewEncrypted'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: lastMessage!.body['algorithm'] != 'plain' ||
|
||||
lastMessage!.body['text'] == null
|
||||
? TextStyle(fontStyle: FontStyle.italic)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
DateFormat(
|
||||
lastMessage!.createdAt.toLocal().day == DateTime.now().day
|
||||
? 'HH:mm'
|
||||
: lastMessage!.createdAt.toLocal().year ==
|
||||
DateTime.now().year
|
||||
? 'MM/dd'
|
||||
: 'yy/MM/dd',
|
||||
).format(lastMessage!.createdAt.toLocal()),
|
||||
style: GoogleFonts.robotoMono(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: otherMember != null
|
||||
? ud.getFromCache(otherMember.accountId)?.avatar
|
||||
: channel.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () => onTap?.call(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -57,11 +57,10 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp =
|
||||
await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/me');
|
||||
_profile = SnChannelMember.fromJson(resp.data);
|
||||
_notifyLevel = _profile!.notify;
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
final resp = await ct.getChannelProfile(_channel!);
|
||||
_profile = resp;
|
||||
_notifyLevel = resp.notify;
|
||||
if (!mounted) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
await ud.getAccount(_profile!.accountId);
|
||||
@ -103,10 +102,12 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete(
|
||||
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me',
|
||||
);
|
||||
await ct.removeLocalChannel(_channel!);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, false);
|
||||
} catch (err) {
|
||||
@ -130,12 +131,15 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
setState(() => _isUpdatingNotifyLevel = true);
|
||||
|
||||
try {
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put(
|
||||
final resp = await sn.client.put(
|
||||
'/cgi/im/channels/${_channel!.keyPath}/members/me/notify',
|
||||
data: {'notify_level': value},
|
||||
);
|
||||
_profile = SnChannelMember.fromJson(resp.data);
|
||||
_notifyLevel = value;
|
||||
await ct.updateChannelProfile(_profile!);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('channelNotifyLevelApplied'.tr());
|
||||
} catch (err) {
|
||||
@ -289,15 +293,14 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: AccountImage(
|
||||
content:
|
||||
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||
content: ud.getFromCache(_profile!.accountId)?.avatar,
|
||||
radius: 18,
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('channelEditProfile').tr(),
|
||||
subtitle: Text(
|
||||
(_profile?.nick?.isEmpty ?? true)
|
||||
? ud.getAccountFromCache(_profile!.accountId)!.nick
|
||||
? ud.getFromCache(_profile!.accountId)!.nick
|
||||
: _profile!.nick!,
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
@ -408,11 +411,14 @@ class _ChannelProfileDetailDialogState
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put(
|
||||
final resp = await sn.client.put(
|
||||
'/cgi/im/channels/${widget.channel.keyPath}/members/me',
|
||||
data: {'nick': _nickController.text},
|
||||
);
|
||||
final out = SnChannelMember.fromJson(resp.data);
|
||||
await ct.updateChannelProfile(out);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
@ -575,11 +581,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
||||
content: ud.getFromCache(member.accountId)?.avatar,
|
||||
),
|
||||
title: Text(
|
||||
ud.getAccountFromCache(member.accountId)?.name ??
|
||||
'unknown'.tr(),
|
||||
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||
),
|
||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||
trailing: SizedBox(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
@ -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,
|
||||
|
@ -111,7 +111,6 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||
child: const Icon(Symbols.close, size: 28),
|
||||
@ -120,90 +119,24 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeStory').tr(),
|
||||
Text('writePost').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeStory'.tr(),
|
||||
tooltip: 'writePost'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'stories',
|
||||
}).then((value) {
|
||||
GoRouter.of(context).pushNamed('postEditor').then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.post_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeArticle').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeArticle'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'articles',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.news),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeQuestion').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeQuestion'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'questions',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.question_answer),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeVideo').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeVideo'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'videos',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.video_call),
|
||||
child: const Icon(Symbols.edit),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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),
|
||||
],
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
@ -36,7 +37,8 @@ import 'package:provider/provider.dart';
|
||||
import 'package:surface/widgets/post/post_poll_editor.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../providers/sn_realm.dart';
|
||||
const kPostTypes = ['Story', 'Article', 'Question', 'Video'];
|
||||
const kPostTypeAliases = ['stories', 'articles', 'questions', 'videos'];
|
||||
|
||||
class PostEditorExtra {
|
||||
final String? text;
|
||||
@ -53,7 +55,7 @@ class PostEditorExtra {
|
||||
}
|
||||
|
||||
class PostEditorScreen extends StatefulWidget {
|
||||
final String mode;
|
||||
final String? mode;
|
||||
final int? postEditId;
|
||||
final int? postReplyId;
|
||||
final int? postRepostId;
|
||||
@ -72,7 +74,10 @@ class PostEditorScreen extends StatefulWidget {
|
||||
State<PostEditorScreen> createState() => _PostEditorScreenState();
|
||||
}
|
||||
|
||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
class _PostEditorScreenState extends State<PostEditorScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController =
|
||||
TabController(length: 4, vsync: this);
|
||||
late final PostWriteController _writeController = PostWriteController(
|
||||
doLoadFromTemporary: widget.postEditId == null,
|
||||
);
|
||||
@ -95,8 +100,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
final beforeId = config.prefs.getInt('int_last_publisher_id');
|
||||
_writeController
|
||||
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
|
||||
_writeController.setPublisher(
|
||||
_publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
|
||||
_publishers?.firstOrNull);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -125,7 +131,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
modifiers: [
|
||||
(!kIsWeb && Platform.isMacOS)
|
||||
? HotKeyModifier.meta
|
||||
: HotKeyModifier.control
|
||||
],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
@ -204,6 +214,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_writeController.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
hotKeyManager.unregister(_pasteHotKey);
|
||||
@ -215,14 +226,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerHotKey();
|
||||
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
||||
context.showErrorDialog('Unknown post type');
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
_writeController.setMode(widget.mode);
|
||||
}
|
||||
_fetchRealms();
|
||||
_fetchPublishers();
|
||||
if (widget.mode != null) {
|
||||
_writeController.setMode(widget.mode!);
|
||||
}
|
||||
_tabController.addListener(() {
|
||||
if (_tabController.indexIsChanging) {
|
||||
_writeController.setMode(kPostTypeAliases[_tabController.index]);
|
||||
}
|
||||
});
|
||||
_writeController.fetchRelatedPost(
|
||||
context,
|
||||
editing: widget.postEditId,
|
||||
@ -232,7 +245,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
if (widget.extraProps != null) {
|
||||
_writeController.contentController.text = widget.extraProps!.text ?? '';
|
||||
_writeController.titleController.text = widget.extraProps!.title ?? '';
|
||||
_writeController.descriptionController.text = widget.extraProps!.description ?? '';
|
||||
_writeController.descriptionController.text =
|
||||
widget.extraProps!.description ?? '';
|
||||
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
||||
}
|
||||
}
|
||||
@ -249,24 +263,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: PostWriteController.kTitleMap[widget.mode]!.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
title: Text(
|
||||
_writeController.title.isNotEmpty
|
||||
? _writeController.title
|
||||
: 'untitled'.tr(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
@ -275,12 +275,31 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom: _writeController.isNotEmpty || widget.mode != null
|
||||
? null
|
||||
: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
for (final type in kPostTypes)
|
||||
Tab(
|
||||
child: Text(
|
||||
'postType$type'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (_writeController.editingPost != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 20, right: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@ -294,13 +313,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 16),
|
||||
const Gap(10),
|
||||
Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
||||
Text('postEditingNotice').tr(args: [
|
||||
'@${_writeController.editingPost!.publisher.name}'
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_writeController.replyingPost != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 20, right: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@ -314,7 +336,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
const Icon(Symbols.reply, size: 16),
|
||||
const Gap(10),
|
||||
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
|
||||
Text('@${_writeController.replyingPost!.publisher.name}')
|
||||
.bold(),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@ -328,7 +351,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
if (_writeController.repostingPost != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 20, right: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@ -342,7 +366,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
const Icon(Symbols.forward, size: 16),
|
||||
const Gap(10),
|
||||
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
|
||||
Text('@${_writeController.repostingPost!.publisher.name}')
|
||||
.bold(),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@ -359,7 +384,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 160),
|
||||
child: StyledWidget(switch (_writeController.mode) {
|
||||
child: switch (_writeController.mode) {
|
||||
'stories' => _PostStoryEditor(
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
@ -381,10 +406,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
onTapRealm: _showRealmPopup,
|
||||
),
|
||||
_ => const Placeholder(),
|
||||
})
|
||||
.padding(top: 8),
|
||||
},
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||
if (_writeController.attachments.isNotEmpty ||
|
||||
_writeController.thumbnail != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
@ -393,16 +418,19 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
attachments: _writeController.attachments,
|
||||
isBusy: _writeController.isBusy,
|
||||
onUpload: (int idx) async {
|
||||
await _writeController.uploadSingleAttachment(context, idx);
|
||||
await _writeController.uploadSingleAttachment(
|
||||
context, idx);
|
||||
},
|
||||
onInsertLink: (int idx) async {
|
||||
_writeController.contentController.text +=
|
||||
'\n';
|
||||
},
|
||||
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
|
||||
onUpdate:
|
||||
(int idx, PostWriteMedia updatedMedia) async {
|
||||
_writeController.setIsBusy(true);
|
||||
try {
|
||||
_writeController.setAttachmentAt(idx, updatedMedia);
|
||||
_writeController.setAttachmentAt(
|
||||
idx, updatedMedia);
|
||||
} finally {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
@ -415,7 +443,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
},
|
||||
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
||||
onUpdateBusy: (state) =>
|
||||
_writeController.setIsBusy(state),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
],
|
||||
@ -426,11 +455,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
if (_writeController.isBusy &&
|
||||
_writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _writeController.progress),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
@ -439,12 +470,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
Container(
|
||||
child: _writeController.temporaryRestored
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 28, right: 22),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
width: 1 /
|
||||
MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -453,7 +486,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
const Icon(Icons.restore, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(child: Text('postLocalDraftRestored').tr()),
|
||||
Expanded(
|
||||
child:
|
||||
Text('postLocalDraftRestored').tr()),
|
||||
InkWell(
|
||||
child: Text('dialogDismiss').tr(),
|
||||
onTap: () {
|
||||
@ -464,8 +499,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
||||
.height(_writeController.temporaryRestored ? 32 : 0,
|
||||
animate: true)
|
||||
.animate(const Duration(milliseconds: 300),
|
||||
Curves.fastLinearToSlowEaseIn),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@ -485,11 +522,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
if (_writeController.mode == 'stories')
|
||||
IconButton(
|
||||
icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary),
|
||||
icon: Icon(Symbols.poll,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: _writeController.poll == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
||||
backgroundColor:
|
||||
_writeController.poll == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer),
|
||||
),
|
||||
onPressed: () {
|
||||
_showPollEditorDialog();
|
||||
@ -497,14 +541,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
if (_writeController.mode == 'articles')
|
||||
IconButton(
|
||||
icon: Icon(Symbols.full_coverage, color: Theme.of(context).colorScheme.primary),
|
||||
icon: Icon(Symbols.full_coverage,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: _writeController.thumbnail == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
||||
backgroundColor:
|
||||
_writeController.thumbnail == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_writeController.thumbnail != null) {
|
||||
if (_writeController.thumbnail !=
|
||||
null) {
|
||||
_writeController.setThumbnail(null);
|
||||
return;
|
||||
}
|
||||
@ -517,7 +569,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: (_writeController.isBusy || _writeController.publisher == null)
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () {
|
||||
_writeController.sendPost(context).then((_) {
|
||||
@ -556,7 +609,8 @@ class _PostPublisherPopup extends StatelessWidget {
|
||||
final List<SnPublisher>? publishers;
|
||||
final Function onUpdate;
|
||||
|
||||
const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
|
||||
const _PostPublisherPopup(
|
||||
{required this.controller, this.publishers, required this.onUpdate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -568,7 +622,9 @@ class _PostPublisherPopup extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.face, size: 24),
|
||||
const Gap(16),
|
||||
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('accountPublishers',
|
||||
style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
ListTile(
|
||||
@ -612,7 +668,8 @@ class _PostRealmPopup extends StatelessWidget {
|
||||
final List<SnRealm>? realms;
|
||||
final Function onUpdate;
|
||||
|
||||
const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate});
|
||||
const _PostRealmPopup(
|
||||
{required this.controller, this.realms, required this.onUpdate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -624,7 +681,8 @@ class _PostRealmPopup extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.face, size: 24),
|
||||
const Gap(16),
|
||||
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
ListTile(
|
||||
@ -665,12 +723,13 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
|
||||
const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
const _PostStoryEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -717,7 +776,8 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
@ -732,8 +792,10 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -749,7 +811,8 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
|
||||
const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
const _PostArticleEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -857,8 +920,10 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -893,7 +958,8 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -906,12 +972,13 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
|
||||
const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
const _PostQuestionEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -958,7 +1025,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
@ -969,7 +1037,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
@ -984,14 +1053,16 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(top: 8),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1001,7 +1072,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
|
||||
const _PostVideoEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
const _PostVideoEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
void _selectVideo(BuildContext context) async {
|
||||
final video = await showDialog<SnAttachment?>(
|
||||
@ -1022,7 +1094,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
builder: (context) => PendingAttachmentAltDialog(
|
||||
media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
@ -1034,7 +1107,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
builder: (context) => PendingAttachmentBoostDialog(
|
||||
media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
@ -1077,7 +1151,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
|
||||
await sn.client
|
||||
.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
|
||||
controller.setVideoAttachment(null);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
@ -1087,143 +1162,159 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Material(
|
||||
elevation: 2,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapPublisher?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.publisher?.avatar,
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Material(
|
||||
elevation: 2,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapPublisher?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.publisher?.avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(11),
|
||||
Material(
|
||||
elevation: 1,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapRealm?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
||||
radius: 14,
|
||||
const Gap(11),
|
||||
Material(
|
||||
elevation: 1,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapRealm?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
||||
radius: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(12),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: [
|
||||
MenuItem(
|
||||
label: 'attachmentSetAlt'.tr(),
|
||||
icon: Symbols.description,
|
||||
onSelected: () {
|
||||
_setAlt(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentBoost'.tr(),
|
||||
icon: Symbols.bolt,
|
||||
onSelected: () {
|
||||
_createBoost(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentSetThumbnail'.tr(),
|
||||
icon: Symbols.image,
|
||||
onSelected: () {
|
||||
_setThumbnail(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentCopyRandomId'.tr(),
|
||||
icon: Symbols.content_copy,
|
||||
onSelected: () {
|
||||
Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
icon: Symbols.delete,
|
||||
onSelected: () => _deleteAttachment(context),
|
||||
),
|
||||
MenuItem(
|
||||
label: 'unlink'.tr(),
|
||||
icon: Symbols.link_off,
|
||||
onSelected: () {
|
||||
controller.setVideoAttachment(null);
|
||||
},
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(6),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(12),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: [
|
||||
MenuItem(
|
||||
label: 'attachmentSetAlt'.tr(),
|
||||
icon: Symbols.description,
|
||||
onSelected: () {
|
||||
_setAlt(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentBoost'.tr(),
|
||||
icon: Symbols.bolt,
|
||||
onSelected: () {
|
||||
_createBoost(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentSetThumbnail'.tr(),
|
||||
icon: Symbols.image,
|
||||
onSelected: () {
|
||||
_setThumbnail(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentCopyRandomId'.tr(),
|
||||
icon: Symbols.content_copy,
|
||||
onSelected: () {
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: controller.videoAttachment!.rid));
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
icon: Symbols.delete,
|
||||
onSelected: () => _deleteAttachment(context),
|
||||
),
|
||||
MenuItem(
|
||||
label: 'unlink'.tr(),
|
||||
icon: Symbols.link_off,
|
||||
onSelected: () {
|
||||
controller.setVideoAttachment(null);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: controller.videoAttachment == null
|
||||
? () => _selectVideo(context)
|
||||
: null,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: controller.videoAttachment == null
|
||||
? Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
const Gap(4),
|
||||
Text('postVideoUpload'.tr()),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AttachmentItem(
|
||||
data: controller.videoAttachment!,
|
||||
heroTag: const Uuid().v4(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: controller.videoAttachment == null
|
||||
? Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
const Gap(4),
|
||||
Text('postVideoUpload'.tr()),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AttachmentItem(
|
||||
data: controller.videoAttachment!,
|
||||
heroTag: const Uuid().v4(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
Future<void> _fetchPublishers() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/publishers?realm=${widget.alias}');
|
||||
final resp =
|
||||
await sn.client.get('/cgi/co/publishers?realm=${widget.alias}');
|
||||
_publishers = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
@ -68,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
Future<void> _fetchChannels() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public');
|
||||
final resp =
|
||||
await sn.client.get('/cgi/im/channels/${widget.alias}/public');
|
||||
_channels = List<SnChannel>.from(
|
||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||
);
|
||||
@ -98,15 +100,32 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle:
|
||||
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||
Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||
Tab(
|
||||
icon: Icon(Symbols.home,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor)),
|
||||
Tab(
|
||||
icon: Icon(Symbols.explore,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor)),
|
||||
Tab(
|
||||
icon: Icon(Symbols.group,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor)),
|
||||
Tab(
|
||||
icon: Icon(Symbols.settings,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -115,7 +134,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
},
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels),
|
||||
_RealmDetailHomeWidget(
|
||||
realm: _realm, publishers: _publishers, channels: _channels),
|
||||
_RealmPostListWidget(realm: _realm),
|
||||
_RealmMemberListWidget(realm: _realm),
|
||||
_RealmSettingsWidget(
|
||||
@ -137,7 +157,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
||||
final List<SnPublisher>? publishers;
|
||||
final List<SnChannel>? channels;
|
||||
|
||||
const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels});
|
||||
const _RealmDetailHomeWidget(
|
||||
{required this.realm, this.publishers, this.channels});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -168,7 +189,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||
child: Text('realmCommunityPublishersHint'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium)
|
||||
.padding(horizontal: 24, vertical: 8),
|
||||
),
|
||||
),
|
||||
@ -199,7 +221,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||
child: Text('realmCommunityPublicChannelsHint'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium)
|
||||
.padding(horizontal: 24, vertical: 8),
|
||||
),
|
||||
),
|
||||
@ -323,10 +346,12 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
||||
try {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _members.length,
|
||||
});
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/id/realms/${widget.realm!.alias}/members',
|
||||
queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _members.length,
|
||||
});
|
||||
|
||||
final out = List<SnRealmMember>.from(
|
||||
resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [],
|
||||
@ -432,14 +457,14 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
||||
content: ud.getFromCache(member.accountId)?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||
),
|
||||
title: Text(
|
||||
ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
|
||||
ud.getFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
|
||||
),
|
||||
subtitle: Text(
|
||||
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.person_remove),
|
||||
|
@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@ -48,6 +49,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
late final SharedPreferences _prefs;
|
||||
String _docBasepath = '/';
|
||||
|
||||
final TextEditingController _customFontController = TextEditingController();
|
||||
final TextEditingController _serverUrlController = TextEditingController();
|
||||
|
||||
@override
|
||||
@ -62,11 +64,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final config = context.read<ConfigProvider>();
|
||||
_prefs = config.prefs;
|
||||
_serverUrlController.text = config.serverUrl;
|
||||
if (_prefs.getString(kAppCustomFonts) != null) {
|
||||
_customFontController.text = _prefs.getString(kAppCustomFonts) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serverUrlController.dispose();
|
||||
_customFontController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -330,6 +336,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.font_download),
|
||||
title: Text('settingsCustomFonts').tr(),
|
||||
subtitle: Text('settingsCustomFontsDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 14),
|
||||
trailing: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_prefs.remove(kAppCustomFonts);
|
||||
context.showSnackbar('settingsCustomFontApplied'.tr());
|
||||
final theme = context.read<ThemeProvider>();
|
||||
_customFontController.clear();
|
||||
theme.reloadTheme();
|
||||
},
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _customFontController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'settingsCustomFontFamily'.tr(),
|
||||
helperText: 'settingsCustomFontFamilyHint'.tr(),
|
||||
prefixIcon: const Icon(Symbols.format_paint),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Symbols.save),
|
||||
onPressed: () {
|
||||
_prefs.setString(
|
||||
kAppCustomFonts,
|
||||
_customFontController.text,
|
||||
);
|
||||
context.showSnackbar('settingsCustomFontApplied'.tr());
|
||||
final theme = context.read<ThemeProvider>();
|
||||
theme.reloadTheme();
|
||||
},
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16, top: 8, bottom: 4),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
@ -534,6 +581,37 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.home_storage),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('cacheSize').tr(),
|
||||
subtitle: FutureBuilder(
|
||||
future: DefaultCacheManager().store.getCacheSize(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || kIsWeb) {
|
||||
return Text('unknown').tr();
|
||||
}
|
||||
return Text(
|
||||
snapshot.data!.formatBytes(),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.cleaning_services),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('cacheDelete').tr(),
|
||||
subtitle: Text('cacheDeleteDescription').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () async {
|
||||
await DefaultCacheManager().emptyCache();
|
||||
if (!context.mounted) return;
|
||||
HapticFeedback.heavyImpact();
|
||||
context.showSnackbar('cacheDeleted'.tr());
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.database),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
@ -618,6 +696,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('runtimeLogsOpen').tr(),
|
||||
subtitle: Text('runtimeLogsDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.receipt_long),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () async {
|
||||
GoRouter.of(context).pushNamed('debugLogging');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('settingsMiscAbout').tr(),
|
||||
subtitle: Text('settingsMiscAboutDescription').tr(),
|
||||
|
@ -51,26 +51,35 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
leading: Icon(Icons.post_add),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('shareIntentPostStory').tr(),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {
|
||||
queryParameters: {
|
||||
'mode': 'stories',
|
||||
},
|
||||
extra: PostEditorExtra(
|
||||
text: value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.where((e) => [
|
||||
SharedMediaType.text,
|
||||
SharedMediaType.url
|
||||
].contains(e.type))
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
attachments: value
|
||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
||||
.contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.where((e) => [
|
||||
SharedMediaType.video,
|
||||
SharedMediaType.file,
|
||||
SharedMediaType.image
|
||||
].contains(e.type))
|
||||
.map((e) =>
|
||||
PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
@ -78,15 +87,18 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
leading: Icon(Icons.chat_outlined),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('shareIntentSendChannel').tr(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _ShareIntentChannelSelect(value: value),
|
||||
builder: (context) =>
|
||||
_ShareIntentChannelSelect(value: value),
|
||||
).then((val) {
|
||||
if (!context.mounted) return;
|
||||
if (val == true) Navigator.pop(context);
|
||||
@ -110,7 +122,8 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
}
|
||||
|
||||
void _initialize() async {
|
||||
_shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) {
|
||||
_shareIntentSubscription =
|
||||
ReceiveSharingIntent.instance.getMediaStream().listen((value) {
|
||||
if (value.isEmpty) return;
|
||||
if (mounted) {
|
||||
_gotoPost(value);
|
||||
@ -157,7 +170,8 @@ class _ShareIntentChannelSelect extends StatefulWidget {
|
||||
const _ShareIntentChannelSelect({required this.value});
|
||||
|
||||
@override
|
||||
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
||||
State<_ShareIntentChannelSelect> createState() =>
|
||||
_ShareIntentChannelSelectState();
|
||||
}
|
||||
|
||||
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
@ -178,8 +192,11 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
final lastMessages = await chan.getLastMessages(channels);
|
||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||
channels.sort((a, b) {
|
||||
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
||||
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
||||
if (_lastMessages!.containsKey(a.id) &&
|
||||
_lastMessages!.containsKey(b.id)) {
|
||||
return _lastMessages![b.id]!
|
||||
.createdAt
|
||||
.compareTo(_lastMessages![a.id]!.createdAt);
|
||||
}
|
||||
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||
@ -232,7 +249,9 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
children: [
|
||||
const Icon(Symbols.chat, size: 24),
|
||||
const Gap(16),
|
||||
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('shareIntentSendChannel',
|
||||
style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
@ -249,29 +268,34 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
final lastMessage = _lastMessages?[channel.id];
|
||||
|
||||
if (channel.type == 1) {
|
||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
final otherMember =
|
||||
channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||
title: Text(
|
||||
ud.getFromCache(otherMember?.accountId)?.nick ??
|
||||
channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
'channelDirectMessageDescription'.tr(args: [
|
||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||
'@${ud.getFromCache(otherMember?.accountId)?.name}',
|
||||
]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||
content:
|
||||
ud.getFromCache(otherMember?.accountId)?.avatar,
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
@ -291,7 +315,7 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
title: Text(channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
@ -316,13 +340,20 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
},
|
||||
extra: ChatRoomScreenExtra(
|
||||
initialText: widget.value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.where((e) => [
|
||||
SharedMediaType.text,
|
||||
SharedMediaType.url
|
||||
].contains(e.type))
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
initialAttachments: widget.value
|
||||
.where((e) =>
|
||||
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.where((e) => [
|
||||
SharedMediaType.video,
|
||||
SharedMediaType.file,
|
||||
SharedMediaType.image
|
||||
].contains(e.type))
|
||||
.map(
|
||||
(e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
|
@ -179,7 +179,9 @@ class _StickerScreenState extends State<StickerScreen>
|
||||
child: InfiniteList(
|
||||
itemCount: _packs.length,
|
||||
onFetchData: _fetchPacks,
|
||||
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!,
|
||||
hasReachedMax:
|
||||
(_totalCount != null && _packs.length >= _totalCount!) ||
|
||||
_tabController.index == 2,
|
||||
isLoading: _isBusy,
|
||||
itemBuilder: (context, idx) {
|
||||
final pack = _packs[idx];
|
||||
@ -282,7 +284,10 @@ class _StickerPackAddPopupState extends State<_StickerPackAddPopup> {
|
||||
);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('stickersAdded'.tr());
|
||||
if (_pack?.stickers != null) stickers.putSticker(_pack!.stickers!);
|
||||
if (_pack?.stickers != null) {
|
||||
stickers.putSticker(
|
||||
_pack!.stickers!.map((ele) => ele.copyWith(pack: _pack!)));
|
||||
}
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
|
@ -11,10 +11,19 @@ class ThemeSet {
|
||||
ThemeSet({required this.light, required this.dark});
|
||||
}
|
||||
|
||||
Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async {
|
||||
Future<ThemeSet> createAppThemeSet(
|
||||
{Color? seedColorOverride, bool? useMaterial3, String? customFonts}) async {
|
||||
return ThemeSet(
|
||||
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
|
||||
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
|
||||
light: await createAppTheme(
|
||||
Brightness.light,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
),
|
||||
dark: await createAppTheme(
|
||||
Brightness.dark,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,24 +31,36 @@ Future<ThemeData> createAppTheme(
|
||||
Brightness brightness, {
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
String? customFonts,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
|
||||
final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo;
|
||||
final seedColor =
|
||||
seedColorString != null ? Color(seedColorString) : Colors.indigo;
|
||||
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: seedColorOverride ?? seedColor,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
final hasAppBarTransparent =
|
||||
prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 =
|
||||
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
|
||||
final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts))
|
||||
?.split(',')
|
||||
.map((ele) => ele.trim())
|
||||
.toList() ??
|
||||
['Nunito'];
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: useM3,
|
||||
colorScheme: colorScheme,
|
||||
brightness: brightness,
|
||||
fontFamily: inUseFonts.firstOrNull,
|
||||
fontFamilyFallback: inUseFonts.sublist(1),
|
||||
iconTheme: IconThemeData(
|
||||
fill: 0,
|
||||
weight: 400,
|
||||
@ -52,8 +73,10 @@ Future<ThemeData> createAppTheme(
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarTransparent ? 0 : null,
|
||||
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
backgroundColor:
|
||||
hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||
foregroundColor:
|
||||
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
@ -67,3 +90,20 @@ Future<ThemeData> createAppTheme(
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
extension HexColor on Color {
|
||||
/// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#".
|
||||
static Color fromHex(String hexString) {
|
||||
final buffer = StringBuffer();
|
||||
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
|
||||
buffer.write(hexString.replaceFirst('#', ''));
|
||||
return Color(int.parse(buffer.toString(), radix: 16));
|
||||
}
|
||||
|
||||
/// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`).
|
||||
String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}'
|
||||
'${alpha.toRadixString(16).padLeft(2, '0')}'
|
||||
'${red.toRadixString(16).padLeft(2, '0')}'
|
||||
'${green.toRadixString(16).padLeft(2, '0')}'
|
||||
'${blue.toRadixString(16).padLeft(2, '0')}';
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ part 'account.freezed.dart';
|
||||
part 'account.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnAccount with _$SnAccount {
|
||||
abstract class SnAccount with _$SnAccount {
|
||||
const SnAccount._();
|
||||
|
||||
const factory SnAccount({
|
||||
@ -16,7 +16,6 @@ class SnAccount with _$SnAccount {
|
||||
required List<SnAccountContact>? contacts,
|
||||
@Default("") String avatar,
|
||||
@Default("") String banner,
|
||||
required String description,
|
||||
required String name,
|
||||
required String nick,
|
||||
@Default({}) Map<String, dynamic> permNodes,
|
||||
@ -35,7 +34,7 @@ class SnAccount with _$SnAccount {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAccountContact with _$SnAccountContact {
|
||||
abstract class SnAccountContact with _$SnAccountContact {
|
||||
const factory SnAccountContact({
|
||||
required int accountId,
|
||||
required String content,
|
||||
@ -54,18 +53,24 @@ class SnAccountContact with _$SnAccountContact {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAccountProfile with _$SnAccountProfile {
|
||||
abstract class SnAccountProfile with _$SnAccountProfile {
|
||||
const factory SnAccountProfile({
|
||||
required int id,
|
||||
required int accountId,
|
||||
required DateTime? birthday,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required int experience,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String description,
|
||||
required String timeZone,
|
||||
required String location,
|
||||
required String pronouns,
|
||||
required String gender,
|
||||
@Default({}) Map<String, String> links,
|
||||
required int experience,
|
||||
required DateTime? lastSeenAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? birthday,
|
||||
required int accountId,
|
||||
}) = _SnAccountProfile;
|
||||
|
||||
factory SnAccountProfile.fromJson(Map<String, Object?> json) =>
|
||||
@ -73,7 +78,7 @@ class SnAccountProfile with _$SnAccountProfile {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnRelationship with _$SnRelationship {
|
||||
abstract class SnRelationship with _$SnRelationship {
|
||||
const factory SnRelationship({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -92,7 +97,7 @@ class SnRelationship with _$SnRelationship {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAccountBadge with _$SnAccountBadge {
|
||||
abstract class SnAccountBadge with _$SnAccountBadge {
|
||||
const factory SnAccountBadge({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -100,6 +105,7 @@ class SnAccountBadge with _$SnAccountBadge {
|
||||
required dynamic deletedAt,
|
||||
required String type,
|
||||
required int accountId,
|
||||
@Default(false) bool isActive,
|
||||
@Default({}) Map<String, dynamic> metadata,
|
||||
}) = _SnAccountBadge;
|
||||
|
||||
@ -108,7 +114,7 @@ class SnAccountBadge with _$SnAccountBadge {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAccountStatusInfo with _$SnAccountStatusInfo {
|
||||
abstract class SnAccountStatusInfo with _$SnAccountStatusInfo {
|
||||
const factory SnAccountStatusInfo({
|
||||
required bool isDisturbable,
|
||||
required bool isOnline,
|
||||
@ -121,7 +127,7 @@ class SnAccountStatusInfo with _$SnAccountStatusInfo {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAbuseReport with _$SnAbuseReport {
|
||||
abstract class SnAbuseReport with _$SnAbuseReport {
|
||||
const factory SnAbuseReport({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,7 @@ part of 'account.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnAccountImpl(
|
||||
_SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -22,7 +21,6 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
.toList(),
|
||||
avatar: json['avatar'] as String? ?? "",
|
||||
banner: json['banner'] as String? ?? "",
|
||||
description: json['description'] as String,
|
||||
name: json['name'] as String,
|
||||
nick: json['nick'] as String,
|
||||
permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {},
|
||||
@ -43,7 +41,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
automatedId: (json['automated_id'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
||||
Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -53,7 +51,6 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
||||
'contacts': instance.contacts?.map((e) => e.toJson()).toList(),
|
||||
'avatar': instance.avatar,
|
||||
'banner': instance.banner,
|
||||
'description': instance.description,
|
||||
'name': instance.name,
|
||||
'nick': instance.nick,
|
||||
'perm_nodes': instance.permNodes,
|
||||
@ -67,9 +64,8 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
||||
'automated_id': instance.automatedId,
|
||||
};
|
||||
|
||||
_$SnAccountContactImpl _$$SnAccountContactImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAccountContactImpl(
|
||||
_SnAccountContact _$SnAccountContactFromJson(Map<String, dynamic> json) =>
|
||||
_SnAccountContact(
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
content: json['content'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
@ -86,8 +82,7 @@ _$SnAccountContactImpl _$$SnAccountContactImplFromJson(
|
||||
: DateTime.parse(json['verified_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAccountContactImplToJson(
|
||||
_$SnAccountContactImpl instance) =>
|
||||
Map<String, dynamic> _$SnAccountContactToJson(_SnAccountContact instance) =>
|
||||
<String, dynamic>{
|
||||
'account_id': instance.accountId,
|
||||
'content': instance.content,
|
||||
@ -101,44 +96,57 @@ Map<String, dynamic> _$$SnAccountContactImplToJson(
|
||||
'verified_at': instance.verifiedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_$SnAccountProfileImpl _$$SnAccountProfileImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAccountProfileImpl(
|
||||
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
|
||||
_SnAccountProfile(
|
||||
id: (json['id'] as num).toInt(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
birthday: json['birthday'] == null
|
||||
? null
|
||||
: DateTime.parse(json['birthday'] as String),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
experience: (json['experience'] as num).toInt(),
|
||||
firstName: json['first_name'] as String,
|
||||
lastName: json['last_name'] as String,
|
||||
description: json['description'] as String,
|
||||
timeZone: json['time_zone'] as String,
|
||||
location: json['location'] as String,
|
||||
pronouns: json['pronouns'] as String,
|
||||
gender: json['gender'] as String,
|
||||
links: (json['links'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
) ??
|
||||
const {},
|
||||
experience: (json['experience'] as num).toInt(),
|
||||
lastSeenAt: json['last_seen_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['last_seen_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
birthday: json['birthday'] == null
|
||||
? null
|
||||
: DateTime.parse(json['birthday'] as String),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAccountProfileImplToJson(
|
||||
_$SnAccountProfileImpl instance) =>
|
||||
Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'account_id': instance.accountId,
|
||||
'birthday': instance.birthday?.toIso8601String(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'experience': instance.experience,
|
||||
'first_name': instance.firstName,
|
||||
'last_name': instance.lastName,
|
||||
'description': instance.description,
|
||||
'time_zone': instance.timeZone,
|
||||
'location': instance.location,
|
||||
'pronouns': instance.pronouns,
|
||||
'gender': instance.gender,
|
||||
'links': instance.links,
|
||||
'experience': instance.experience,
|
||||
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'birthday': instance.birthday?.toIso8601String(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnRelationshipImpl _$$SnRelationshipImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnRelationshipImpl(
|
||||
_SnRelationship _$SnRelationshipFromJson(Map<String, dynamic> json) =>
|
||||
_SnRelationship(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -157,8 +165,7 @@ _$SnRelationshipImpl _$$SnRelationshipImplFromJson(Map<String, dynamic> json) =>
|
||||
permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnRelationshipImplToJson(
|
||||
_$SnRelationshipImpl instance) =>
|
||||
Map<String, dynamic> _$SnRelationshipToJson(_SnRelationship instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -172,19 +179,19 @@ Map<String, dynamic> _$$SnRelationshipImplToJson(
|
||||
'perm_nodes': instance.permNodes,
|
||||
};
|
||||
|
||||
_$SnAccountBadgeImpl _$$SnAccountBadgeImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnAccountBadgeImpl(
|
||||
_SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) =>
|
||||
_SnAccountBadge(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'],
|
||||
type: json['type'] as String,
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
isActive: json['is_active'] as bool? ?? false,
|
||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAccountBadgeImplToJson(
|
||||
_$SnAccountBadgeImpl instance) =>
|
||||
Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -192,12 +199,12 @@ Map<String, dynamic> _$$SnAccountBadgeImplToJson(
|
||||
'deleted_at': instance.deletedAt,
|
||||
'type': instance.type,
|
||||
'account_id': instance.accountId,
|
||||
'is_active': instance.isActive,
|
||||
'metadata': instance.metadata,
|
||||
};
|
||||
|
||||
_$SnAccountStatusInfoImpl _$$SnAccountStatusInfoImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAccountStatusInfoImpl(
|
||||
_SnAccountStatusInfo _$SnAccountStatusInfoFromJson(Map<String, dynamic> json) =>
|
||||
_SnAccountStatusInfo(
|
||||
isDisturbable: json['is_disturbable'] as bool,
|
||||
isOnline: json['is_online'] as bool,
|
||||
lastSeenAt: json['last_seen_at'] == null
|
||||
@ -206,8 +213,8 @@ _$SnAccountStatusInfoImpl _$$SnAccountStatusInfoImplFromJson(
|
||||
status: json['status'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAccountStatusInfoImplToJson(
|
||||
_$SnAccountStatusInfoImpl instance) =>
|
||||
Map<String, dynamic> _$SnAccountStatusInfoToJson(
|
||||
_SnAccountStatusInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'is_disturbable': instance.isDisturbable,
|
||||
'is_online': instance.isOnline,
|
||||
@ -215,8 +222,8 @@ Map<String, dynamic> _$$SnAccountStatusInfoImplToJson(
|
||||
'status': instance.status,
|
||||
};
|
||||
|
||||
_$SnAbuseReportImpl _$$SnAbuseReportImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnAbuseReportImpl(
|
||||
_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
|
||||
_SnAbuseReport(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -229,7 +236,7 @@ _$SnAbuseReportImpl _$$SnAbuseReportImplFromJson(Map<String, dynamic> json) =>
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAbuseReportImplToJson(_$SnAbuseReportImpl instance) =>
|
||||
Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@ -12,7 +12,7 @@ enum SnMediaType {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachment with _$SnAttachment {
|
||||
abstract class SnAttachment with _$SnAttachment {
|
||||
const SnAttachment._();
|
||||
|
||||
const factory SnAttachment({
|
||||
@ -65,7 +65,7 @@ class SnAttachment with _$SnAttachment {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachmentFragment with _$SnAttachmentFragment {
|
||||
abstract class SnAttachmentFragment with _$SnAttachmentFragment {
|
||||
const SnAttachmentFragment._();
|
||||
|
||||
const factory SnAttachmentFragment({
|
||||
@ -96,7 +96,7 @@ class SnAttachmentFragment with _$SnAttachmentFragment {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachmentPool with _$SnAttachmentPool {
|
||||
abstract class SnAttachmentPool with _$SnAttachmentPool {
|
||||
const factory SnAttachmentPool({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -113,7 +113,7 @@ class SnAttachmentPool with _$SnAttachmentPool {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||
abstract class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||
const factory SnAttachmentDestination({
|
||||
@Default(0) int id,
|
||||
required String type,
|
||||
@ -126,7 +126,7 @@ class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||
abstract class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||
const factory SnAttachmentBoost({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -143,7 +143,7 @@ class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnSticker with _$SnSticker {
|
||||
abstract class SnSticker with _$SnSticker {
|
||||
const factory SnSticker({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -162,7 +162,7 @@ class SnSticker with _$SnSticker {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnStickerPack with _$SnStickerPack {
|
||||
abstract class SnStickerPack with _$SnStickerPack {
|
||||
const factory SnStickerPack({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -179,7 +179,7 @@ class SnStickerPack with _$SnStickerPack {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachmentBilling with _$SnAttachmentBilling {
|
||||
abstract class SnAttachmentBilling with _$SnAttachmentBilling {
|
||||
const factory SnAttachmentBilling({
|
||||
required int currentBytes,
|
||||
required int discountFileSize,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,8 @@ part of 'attachment.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnAttachmentImpl(
|
||||
_SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
|
||||
_SnAttachment(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -57,7 +57,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
||||
Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -92,9 +92,9 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
||||
'metadata': instance.metadata,
|
||||
};
|
||||
|
||||
_$SnAttachmentFragmentImpl _$$SnAttachmentFragmentImplFromJson(
|
||||
_SnAttachmentFragment _$SnAttachmentFragmentFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAttachmentFragmentImpl(
|
||||
_SnAttachmentFragment(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -119,8 +119,8 @@ _$SnAttachmentFragmentImpl _$$SnAttachmentFragmentImplFromJson(
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentFragmentImplToJson(
|
||||
_$SnAttachmentFragmentImpl instance) =>
|
||||
Map<String, dynamic> _$SnAttachmentFragmentToJson(
|
||||
_SnAttachmentFragment instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -138,9 +138,8 @@ Map<String, dynamic> _$$SnAttachmentFragmentImplToJson(
|
||||
'file_chunks_missing': instance.fileChunksMissing,
|
||||
};
|
||||
|
||||
_$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAttachmentPoolImpl(
|
||||
_SnAttachmentPool _$SnAttachmentPoolFromJson(Map<String, dynamic> json) =>
|
||||
_SnAttachmentPool(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -154,8 +153,7 @@ _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson(
|
||||
accountId: (json['account_id'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
|
||||
_$SnAttachmentPoolImpl instance) =>
|
||||
Map<String, dynamic> _$SnAttachmentPoolToJson(_SnAttachmentPool instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -168,9 +166,9 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson(
|
||||
_SnAttachmentDestination _$SnAttachmentDestinationFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAttachmentDestinationImpl(
|
||||
_SnAttachmentDestination(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
type: json['type'] as String,
|
||||
label: json['label'] as String,
|
||||
@ -178,8 +176,8 @@ _$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson(
|
||||
isBoost: json['is_boost'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentDestinationImplToJson(
|
||||
_$SnAttachmentDestinationImpl instance) =>
|
||||
Map<String, dynamic> _$SnAttachmentDestinationToJson(
|
||||
_SnAttachmentDestination instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': instance.type,
|
||||
@ -188,9 +186,8 @@ Map<String, dynamic> _$$SnAttachmentDestinationImplToJson(
|
||||
'is_boost': instance.isBoost,
|
||||
};
|
||||
|
||||
_$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAttachmentBoostImpl(
|
||||
_SnAttachmentBoost _$SnAttachmentBoostFromJson(Map<String, dynamic> json) =>
|
||||
_SnAttachmentBoost(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -205,8 +202,7 @@ _$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson(
|
||||
account: (json['account'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
|
||||
_$SnAttachmentBoostImpl instance) =>
|
||||
Map<String, dynamic> _$SnAttachmentBoostToJson(_SnAttachmentBoost instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -219,8 +215,7 @@ Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
|
||||
'account': instance.account,
|
||||
};
|
||||
|
||||
_$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnStickerImpl(
|
||||
_SnSticker _$SnStickerFromJson(Map<String, dynamic> json) => _SnSticker(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -237,7 +232,7 @@ _$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) =>
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) =>
|
||||
Map<String, dynamic> _$SnStickerToJson(_SnSticker instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -252,8 +247,8 @@ Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) =>
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnStickerPackImpl(
|
||||
_SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) =>
|
||||
_SnStickerPack(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -269,7 +264,7 @@ _$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) =>
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
|
||||
Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -282,16 +277,15 @@ Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnAttachmentBillingImpl _$$SnAttachmentBillingImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAttachmentBillingImpl(
|
||||
_SnAttachmentBilling _$SnAttachmentBillingFromJson(Map<String, dynamic> json) =>
|
||||
_SnAttachmentBilling(
|
||||
currentBytes: (json['current_bytes'] as num).toInt(),
|
||||
discountFileSize: (json['discount_file_size'] as num).toInt(),
|
||||
includedRatio: (json['included_ratio'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentBillingImplToJson(
|
||||
_$SnAttachmentBillingImpl instance) =>
|
||||
Map<String, dynamic> _$SnAttachmentBillingToJson(
|
||||
_SnAttachmentBilling instance) =>
|
||||
<String, dynamic>{
|
||||
'current_bytes': instance.currentBytes,
|
||||
'discount_file_size': instance.discountFileSize,
|
||||
|
@ -4,7 +4,7 @@ part 'auth.freezed.dart';
|
||||
part 'auth.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnAuthResult with _$SnAuthResult {
|
||||
abstract class SnAuthResult with _$SnAuthResult {
|
||||
const factory SnAuthResult({
|
||||
required bool isFinished,
|
||||
required SnAuthTicket? ticket,
|
||||
@ -15,7 +15,7 @@ class SnAuthResult with _$SnAuthResult {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAuthTicket with _$SnAuthTicket {
|
||||
abstract class SnAuthTicket with _$SnAuthTicket {
|
||||
const factory SnAuthTicket({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -41,7 +41,7 @@ class SnAuthTicket with _$SnAuthTicket {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAuthFactor with _$SnAuthFactor {
|
||||
abstract class SnAuthFactor with _$SnAuthFactor {
|
||||
const factory SnAuthFactor({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,22 +6,22 @@ part of 'auth.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnAuthResultImpl _$$SnAuthResultImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnAuthResultImpl(
|
||||
_SnAuthResult _$SnAuthResultFromJson(Map<String, dynamic> json) =>
|
||||
_SnAuthResult(
|
||||
isFinished: json['is_finished'] as bool,
|
||||
ticket: json['ticket'] == null
|
||||
? null
|
||||
: SnAuthTicket.fromJson(json['ticket'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAuthResultImplToJson(_$SnAuthResultImpl instance) =>
|
||||
Map<String, dynamic> _$SnAuthResultToJson(_SnAuthResult instance) =>
|
||||
<String, dynamic>{
|
||||
'is_finished': instance.isFinished,
|
||||
'ticket': instance.ticket?.toJson(),
|
||||
};
|
||||
|
||||
_$SnAuthTicketImpl _$$SnAuthTicketImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnAuthTicketImpl(
|
||||
_SnAuthTicket _$SnAuthTicketFromJson(Map<String, dynamic> json) =>
|
||||
_SnAuthTicket(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -52,7 +52,7 @@ _$SnAuthTicketImpl _$$SnAuthTicketImplFromJson(Map<String, dynamic> json) =>
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAuthTicketImplToJson(_$SnAuthTicketImpl instance) =>
|
||||
Map<String, dynamic> _$SnAuthTicketToJson(_SnAuthTicket instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -73,8 +73,8 @@ Map<String, dynamic> _$$SnAuthTicketImplToJson(_$SnAuthTicketImpl instance) =>
|
||||
'factor_trail': instance.factorTrail,
|
||||
};
|
||||
|
||||
_$SnAuthFactorImpl _$$SnAuthFactorImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnAuthFactorImpl(
|
||||
_SnAuthFactor _$SnAuthFactorFromJson(Map<String, dynamic> json) =>
|
||||
_SnAuthFactor(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -86,7 +86,7 @@ _$SnAuthFactorImpl _$$SnAuthFactorImplFromJson(Map<String, dynamic> json) =>
|
||||
accountId: (json['account_id'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAuthFactorImplToJson(_$SnAuthFactorImpl instance) =>
|
||||
Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@ -8,7 +8,7 @@ part 'chat.freezed.dart';
|
||||
part 'chat.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnChannel with _$SnChannel {
|
||||
abstract class SnChannel with _$SnChannel {
|
||||
const SnChannel._();
|
||||
|
||||
const factory SnChannel({
|
||||
@ -37,7 +37,7 @@ class SnChannel with _$SnChannel {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnChannelMember with _$SnChannelMember {
|
||||
abstract class SnChannelMember with _$SnChannelMember {
|
||||
const SnChannelMember._();
|
||||
|
||||
const factory SnChannelMember({
|
||||
@ -61,7 +61,7 @@ class SnChannelMember with _$SnChannelMember {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnChatMessage with _$SnChatMessage {
|
||||
abstract class SnChatMessage with _$SnChatMessage {
|
||||
const SnChatMessage._();
|
||||
|
||||
const factory SnChatMessage({
|
||||
@ -86,7 +86,7 @@ class SnChatMessage with _$SnChatMessage {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnChatMessagePreload with _$SnChatMessagePreload {
|
||||
abstract class SnChatMessagePreload with _$SnChatMessagePreload {
|
||||
const SnChatMessagePreload._();
|
||||
|
||||
const factory SnChatMessagePreload({
|
||||
@ -99,7 +99,7 @@ class SnChatMessagePreload with _$SnChatMessagePreload {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnChatCall with _$SnChatCall {
|
||||
abstract class SnChatCall with _$SnChatCall {
|
||||
const factory SnChatCall({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,7 @@ part of 'chat.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnChannelImpl _$$SnChannelImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnChannelImpl(
|
||||
_SnChannel _$SnChannelFromJson(Map<String, dynamic> json) => _SnChannel(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -31,7 +30,7 @@ _$SnChannelImpl _$$SnChannelImplFromJson(Map<String, dynamic> json) =>
|
||||
isCommunity: json['is_community'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnChannelImplToJson(_$SnChannelImpl instance) =>
|
||||
Map<String, dynamic> _$SnChannelToJson(_SnChannel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -50,9 +49,8 @@ Map<String, dynamic> _$$SnChannelImplToJson(_$SnChannelImpl instance) =>
|
||||
'is_community': instance.isCommunity,
|
||||
};
|
||||
|
||||
_$SnChannelMemberImpl _$$SnChannelMemberImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnChannelMemberImpl(
|
||||
_SnChannelMember _$SnChannelMemberFromJson(Map<String, dynamic> json) =>
|
||||
_SnChannelMember(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -74,8 +72,7 @@ _$SnChannelMemberImpl _$$SnChannelMemberImplFromJson(
|
||||
events: json['events'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnChannelMemberImplToJson(
|
||||
_$SnChannelMemberImpl instance) =>
|
||||
Map<String, dynamic> _$SnChannelMemberToJson(_SnChannelMember instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -92,8 +89,8 @@ Map<String, dynamic> _$$SnChannelMemberImplToJson(
|
||||
'events': instance.events,
|
||||
};
|
||||
|
||||
_$SnChatMessageImpl _$$SnChatMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnChatMessageImpl(
|
||||
_SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>
|
||||
_SnChatMessage(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -115,7 +112,7 @@ _$SnChatMessageImpl _$$SnChatMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
json['preload'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) =>
|
||||
Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -133,9 +130,9 @@ Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) =>
|
||||
'preload': instance.preload?.toJson(),
|
||||
};
|
||||
|
||||
_$SnChatMessagePreloadImpl _$$SnChatMessagePreloadImplFromJson(
|
||||
_SnChatMessagePreload _$SnChatMessagePreloadFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnChatMessagePreloadImpl(
|
||||
_SnChatMessagePreload(
|
||||
attachments: (json['attachments'] as List<dynamic>?)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
@ -146,15 +143,14 @@ _$SnChatMessagePreloadImpl _$$SnChatMessagePreloadImplFromJson(
|
||||
: SnChatMessage.fromJson(json['quote_event'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnChatMessagePreloadImplToJson(
|
||||
_$SnChatMessagePreloadImpl instance) =>
|
||||
Map<String, dynamic> _$SnChatMessagePreloadToJson(
|
||||
_SnChatMessagePreload instance) =>
|
||||
<String, dynamic>{
|
||||
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||
'quote_event': instance.quoteEvent?.toJson(),
|
||||
};
|
||||
|
||||
_$SnChatCallImpl _$$SnChatCallImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnChatCallImpl(
|
||||
_SnChatCall _$SnChatCallFromJson(Map<String, dynamic> json) => _SnChatCall(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -172,7 +168,7 @@ _$SnChatCallImpl _$$SnChatCallImplFromJson(Map<String, dynamic> json) =>
|
||||
participants: json['participants'] as List<dynamic>? ?? const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnChatCallImplToJson(_$SnChatCallImpl instance) =>
|
||||
Map<String, dynamic> _$SnChatCallToJson(_SnChatCall instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@ -14,7 +14,7 @@ final List<String> kCheckInResultTierSymbols = [
|
||||
].map((e) => e.tr()).toList();
|
||||
|
||||
@freezed
|
||||
class SnCheckInRecord with _$SnCheckInRecord {
|
||||
abstract class SnCheckInRecord with _$SnCheckInRecord {
|
||||
const SnCheckInRecord._();
|
||||
|
||||
const factory SnCheckInRecord({
|
||||
|
@ -1,3 +1,4 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
@ -9,128 +10,81 @@ part of 'check_in.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) {
|
||||
return _SnCheckInRecord.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnCheckInRecord {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
int get resultTier => throw _privateConstructorUsedError;
|
||||
int get resultExperience => throw _privateConstructorUsedError;
|
||||
double get resultCoin => throw _privateConstructorUsedError;
|
||||
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnCheckInRecord to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
int get resultTier;
|
||||
int get resultExperience;
|
||||
double get resultCoin;
|
||||
List<int> get resultModifiers;
|
||||
int get accountId;
|
||||
|
||||
/// Create a copy of SnCheckInRecord
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnCheckInRecordCopyWith<SnCheckInRecord> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnCheckInRecordCopyWith<$Res> {
|
||||
factory $SnCheckInRecordCopyWith(
|
||||
SnCheckInRecord value, $Res Function(SnCheckInRecord) then) =
|
||||
_$SnCheckInRecordCopyWithImpl<$Res, SnCheckInRecord>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
implements $SnCheckInRecordCopyWith<$Res> {
|
||||
_$SnCheckInRecordCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnCheckInRecord
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCheckInRecordCopyWith<SnCheckInRecord> get copyWith =>
|
||||
_$SnCheckInRecordCopyWithImpl<SnCheckInRecord>(
|
||||
this as SnCheckInRecord, _$identity);
|
||||
|
||||
/// Serializes this SnCheckInRecord to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
resultTier: null == resultTier
|
||||
? _value.resultTier
|
||||
: resultTier // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultExperience: null == resultExperience
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value.resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnCheckInRecord &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.resultTier, resultTier) ||
|
||||
other.resultTier == resultTier) &&
|
||||
(identical(other.resultExperience, resultExperience) ||
|
||||
other.resultExperience == resultExperience) &&
|
||||
(identical(other.resultCoin, resultCoin) ||
|
||||
other.resultCoin == resultCoin) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.resultModifiers, resultModifiers) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
resultTier,
|
||||
resultExperience,
|
||||
resultCoin,
|
||||
const DeepCollectionEquality().hash(resultModifiers),
|
||||
accountId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
||||
implements $SnCheckInRecordCopyWith<$Res> {
|
||||
factory _$$SnCheckInRecordImplCopyWith(_$SnCheckInRecordImpl value,
|
||||
$Res Function(_$SnCheckInRecordImpl) then) =
|
||||
__$$SnCheckInRecordImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
abstract mixin class $SnCheckInRecordCopyWith<$Res> {
|
||||
factory $SnCheckInRecordCopyWith(
|
||||
SnCheckInRecord value, $Res Function(SnCheckInRecord) _then) =
|
||||
_$SnCheckInRecordCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
@ -145,12 +99,12 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
extends _$SnCheckInRecordCopyWithImpl<$Res, _$SnCheckInRecordImpl>
|
||||
implements _$$SnCheckInRecordImplCopyWith<$Res> {
|
||||
__$$SnCheckInRecordImplCopyWithImpl(
|
||||
_$SnCheckInRecordImpl _value, $Res Function(_$SnCheckInRecordImpl) _then)
|
||||
: super(_value, _then);
|
||||
class _$SnCheckInRecordCopyWithImpl<$Res>
|
||||
implements $SnCheckInRecordCopyWith<$Res> {
|
||||
_$SnCheckInRecordCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnCheckInRecord _self;
|
||||
final $Res Function(SnCheckInRecord) _then;
|
||||
|
||||
/// Create a copy of SnCheckInRecord
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -167,41 +121,41 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_$SnCheckInRecordImpl(
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
resultTier: null == resultTier
|
||||
? _value.resultTier
|
||||
? _self.resultTier
|
||||
: resultTier // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultExperience: null == resultExperience
|
||||
? _value.resultExperience
|
||||
? _self.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
? _self.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value._resultModifiers
|
||||
? _self.resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
@ -210,8 +164,8 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
const _$SnCheckInRecordImpl(
|
||||
class _SnCheckInRecord extends SnCheckInRecord {
|
||||
const _SnCheckInRecord(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@ -223,9 +177,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
required this.accountId})
|
||||
: _resultModifiers = resultModifiers,
|
||||
super._();
|
||||
|
||||
factory _$SnCheckInRecordImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnCheckInRecordImplFromJson(json);
|
||||
factory _SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnCheckInRecordFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@ -252,16 +205,26 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
@override
|
||||
final int accountId;
|
||||
|
||||
/// Create a copy of SnCheckInRecord
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnCheckInRecordCopyWith<_SnCheckInRecord> get copyWith =>
|
||||
__$SnCheckInRecordCopyWithImpl<_SnCheckInRecord>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnCheckInRecordToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnCheckInRecordImpl &&
|
||||
other is _SnCheckInRecord &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
@ -295,62 +258,94 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
const DeepCollectionEquality().hash(_resultModifiers),
|
||||
accountId);
|
||||
|
||||
/// Create a copy of SnCheckInRecord
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnCheckInRecordImplCopyWith<_$SnCheckInRecordImpl> get copyWith =>
|
||||
__$$SnCheckInRecordImplCopyWithImpl<_$SnCheckInRecordImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnCheckInRecordImplToJson(
|
||||
this,
|
||||
);
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
const factory _SnCheckInRecord(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final int resultTier,
|
||||
required final int resultExperience,
|
||||
required final double resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required final int accountId}) = _$SnCheckInRecordImpl;
|
||||
const _SnCheckInRecord._() : super._();
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnCheckInRecordCopyWith<$Res>
|
||||
implements $SnCheckInRecordCopyWith<$Res> {
|
||||
factory _$SnCheckInRecordCopyWith(
|
||||
_SnCheckInRecord value, $Res Function(_SnCheckInRecord) _then) =
|
||||
__$SnCheckInRecordCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
factory _SnCheckInRecord.fromJson(Map<String, dynamic> json) =
|
||||
_$SnCheckInRecordImpl.fromJson;
|
||||
/// @nodoc
|
||||
class __$SnCheckInRecordCopyWithImpl<$Res>
|
||||
implements _$SnCheckInRecordCopyWith<$Res> {
|
||||
__$SnCheckInRecordCopyWithImpl(this._self, this._then);
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
@override
|
||||
int get resultTier;
|
||||
@override
|
||||
int get resultExperience;
|
||||
@override
|
||||
double get resultCoin;
|
||||
@override
|
||||
List<int> get resultModifiers;
|
||||
@override
|
||||
int get accountId;
|
||||
final _SnCheckInRecord _self;
|
||||
final $Res Function(_SnCheckInRecord) _then;
|
||||
|
||||
/// Create a copy of SnCheckInRecord
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnCheckInRecordImplCopyWith<_$SnCheckInRecordImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_SnCheckInRecord(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
resultTier: null == resultTier
|
||||
? _self.resultTier
|
||||
: resultTier // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultExperience: null == resultExperience
|
||||
? _self.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _self.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _self._resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@ -6,9 +6,8 @@ part of 'check_in.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnCheckInRecordImpl(
|
||||
_SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) =>
|
||||
_SnCheckInRecord(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -24,8 +23,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnCheckInRecordImplToJson(
|
||||
_$SnCheckInRecordImpl instance) =>
|
||||
Map<String, dynamic> _$SnCheckInRecordToJson(_SnCheckInRecord instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
18
lib/types/keypair.dart
Normal file
18
lib/types/keypair.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'keypair.freezed.dart';
|
||||
part 'keypair.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class SnKeyPair with _$SnKeyPair {
|
||||
const factory SnKeyPair({
|
||||
required String id,
|
||||
required int accountId,
|
||||
required String publicKey,
|
||||
bool? isActive,
|
||||
String? privateKey,
|
||||
}) = _SnKeyPair;
|
||||
|
||||
factory SnKeyPair.fromJson(Map<String, Object?> json) =>
|
||||
_$SnKeyPairFromJson(json);
|
||||
}
|
241
lib/types/keypair.freezed.dart
Normal file
241
lib/types/keypair.freezed.dart
Normal file
@ -0,0 +1,241 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'keypair.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnKeyPair {
|
||||
String get id;
|
||||
int get accountId;
|
||||
String get publicKey;
|
||||
bool? get isActive;
|
||||
String? get privateKey;
|
||||
|
||||
/// Create a copy of SnKeyPair
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnKeyPairCopyWith<SnKeyPair> get copyWith =>
|
||||
_$SnKeyPairCopyWithImpl<SnKeyPair>(this as SnKeyPair, _$identity);
|
||||
|
||||
/// Serializes this SnKeyPair to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnKeyPair &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.publicKey, publicKey) ||
|
||||
other.publicKey == publicKey) &&
|
||||
(identical(other.isActive, isActive) ||
|
||||
other.isActive == isActive) &&
|
||||
(identical(other.privateKey, privateKey) ||
|
||||
other.privateKey == privateKey));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnKeyPairCopyWith<$Res> {
|
||||
factory $SnKeyPairCopyWith(SnKeyPair value, $Res Function(SnKeyPair) _then) =
|
||||
_$SnKeyPairCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
int accountId,
|
||||
String publicKey,
|
||||
bool? isActive,
|
||||
String? privateKey});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnKeyPairCopyWithImpl<$Res> implements $SnKeyPairCopyWith<$Res> {
|
||||
_$SnKeyPairCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnKeyPair _self;
|
||||
final $Res Function(SnKeyPair) _then;
|
||||
|
||||
/// Create a copy of SnKeyPair
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? accountId = null,
|
||||
Object? publicKey = null,
|
||||
Object? isActive = freezed,
|
||||
Object? privateKey = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
publicKey: null == publicKey
|
||||
? _self.publicKey
|
||||
: publicKey // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isActive: freezed == isActive
|
||||
? _self.isActive
|
||||
: isActive // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
privateKey: freezed == privateKey
|
||||
? _self.privateKey
|
||||
: privateKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnKeyPair implements SnKeyPair {
|
||||
const _SnKeyPair(
|
||||
{required this.id,
|
||||
required this.accountId,
|
||||
required this.publicKey,
|
||||
this.isActive,
|
||||
this.privateKey});
|
||||
factory _SnKeyPair.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnKeyPairFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
final String publicKey;
|
||||
@override
|
||||
final bool? isActive;
|
||||
@override
|
||||
final String? privateKey;
|
||||
|
||||
/// Create a copy of SnKeyPair
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnKeyPairCopyWith<_SnKeyPair> get copyWith =>
|
||||
__$SnKeyPairCopyWithImpl<_SnKeyPair>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnKeyPairToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnKeyPair &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.publicKey, publicKey) ||
|
||||
other.publicKey == publicKey) &&
|
||||
(identical(other.isActive, isActive) ||
|
||||
other.isActive == isActive) &&
|
||||
(identical(other.privateKey, privateKey) ||
|
||||
other.privateKey == privateKey));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnKeyPairCopyWith<$Res>
|
||||
implements $SnKeyPairCopyWith<$Res> {
|
||||
factory _$SnKeyPairCopyWith(
|
||||
_SnKeyPair value, $Res Function(_SnKeyPair) _then) =
|
||||
__$SnKeyPairCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
int accountId,
|
||||
String publicKey,
|
||||
bool? isActive,
|
||||
String? privateKey});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnKeyPairCopyWithImpl<$Res> implements _$SnKeyPairCopyWith<$Res> {
|
||||
__$SnKeyPairCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnKeyPair _self;
|
||||
final $Res Function(_SnKeyPair) _then;
|
||||
|
||||
/// Create a copy of SnKeyPair
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? accountId = null,
|
||||
Object? publicKey = null,
|
||||
Object? isActive = freezed,
|
||||
Object? privateKey = freezed,
|
||||
}) {
|
||||
return _then(_SnKeyPair(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
publicKey: null == publicKey
|
||||
? _self.publicKey
|
||||
: publicKey // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isActive: freezed == isActive
|
||||
? _self.isActive
|
||||
: isActive // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
privateKey: freezed == privateKey
|
||||
? _self.privateKey
|
||||
: privateKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
24
lib/types/keypair.g.dart
Normal file
24
lib/types/keypair.g.dart
Normal file
@ -0,0 +1,24 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'keypair.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnKeyPair _$SnKeyPairFromJson(Map<String, dynamic> json) => _SnKeyPair(
|
||||
id: json['id'] as String,
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
publicKey: json['public_key'] as String,
|
||||
isActive: json['is_active'] as bool?,
|
||||
privateKey: json['private_key'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnKeyPairToJson(_SnKeyPair instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'account_id': instance.accountId,
|
||||
'public_key': instance.publicKey,
|
||||
'is_active': instance.isActive,
|
||||
'private_key': instance.privateKey,
|
||||
};
|
@ -4,7 +4,7 @@ part 'link.g.dart';
|
||||
part 'link.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class SnLinkMeta with _$SnLinkMeta {
|
||||
abstract class SnLinkMeta with _$SnLinkMeta {
|
||||
const SnLinkMeta._();
|
||||
|
||||
const factory SnLinkMeta({
|
||||
|
@ -1,3 +1,4 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
@ -9,332 +10,41 @@ part of 'link.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnLinkMeta _$SnLinkMetaFromJson(Map<String, dynamic> json) {
|
||||
return _SnLinkMeta.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnLinkMeta {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
String get entryId => throw _privateConstructorUsedError;
|
||||
String? get icon => throw _privateConstructorUsedError;
|
||||
String get url => throw _privateConstructorUsedError;
|
||||
String? get title => throw _privateConstructorUsedError;
|
||||
String? get image => throw _privateConstructorUsedError;
|
||||
String? get video => throw _privateConstructorUsedError;
|
||||
String? get audio => throw _privateConstructorUsedError;
|
||||
String? get description => throw _privateConstructorUsedError;
|
||||
String? get siteName => throw _privateConstructorUsedError;
|
||||
String? get type => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnLinkMeta to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get entryId;
|
||||
String? get icon;
|
||||
String get url;
|
||||
String? get title;
|
||||
String? get image;
|
||||
String? get video;
|
||||
String? get audio;
|
||||
String? get description;
|
||||
String? get siteName;
|
||||
String? get type;
|
||||
|
||||
/// Create a copy of SnLinkMeta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnLinkMetaCopyWith<SnLinkMeta> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
_$SnLinkMetaCopyWithImpl<SnLinkMeta>(this as SnLinkMeta, _$identity);
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnLinkMetaCopyWith<$Res> {
|
||||
factory $SnLinkMetaCopyWith(
|
||||
SnLinkMeta value, $Res Function(SnLinkMeta) then) =
|
||||
_$SnLinkMetaCopyWithImpl<$Res, SnLinkMeta>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String entryId,
|
||||
String? icon,
|
||||
String url,
|
||||
String? title,
|
||||
String? image,
|
||||
String? video,
|
||||
String? audio,
|
||||
String? description,
|
||||
String? siteName,
|
||||
String? type});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnLinkMetaCopyWithImpl<$Res, $Val extends SnLinkMeta>
|
||||
implements $SnLinkMetaCopyWith<$Res> {
|
||||
_$SnLinkMetaCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnLinkMeta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? entryId = null,
|
||||
Object? icon = freezed,
|
||||
Object? url = null,
|
||||
Object? title = freezed,
|
||||
Object? image = freezed,
|
||||
Object? video = freezed,
|
||||
Object? audio = freezed,
|
||||
Object? description = freezed,
|
||||
Object? siteName = freezed,
|
||||
Object? type = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
entryId: null == entryId
|
||||
? _value.entryId
|
||||
: entryId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: freezed == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
image: freezed == image
|
||||
? _value.image
|
||||
: image // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
audio: freezed == audio
|
||||
? _value.audio
|
||||
: audio // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
description: freezed == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
siteName: freezed == siteName
|
||||
? _value.siteName
|
||||
: siteName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
type: freezed == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnLinkMetaImplCopyWith<$Res>
|
||||
implements $SnLinkMetaCopyWith<$Res> {
|
||||
factory _$$SnLinkMetaImplCopyWith(
|
||||
_$SnLinkMetaImpl value, $Res Function(_$SnLinkMetaImpl) then) =
|
||||
__$$SnLinkMetaImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String entryId,
|
||||
String? icon,
|
||||
String url,
|
||||
String? title,
|
||||
String? image,
|
||||
String? video,
|
||||
String? audio,
|
||||
String? description,
|
||||
String? siteName,
|
||||
String? type});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnLinkMetaImplCopyWithImpl<$Res>
|
||||
extends _$SnLinkMetaCopyWithImpl<$Res, _$SnLinkMetaImpl>
|
||||
implements _$$SnLinkMetaImplCopyWith<$Res> {
|
||||
__$$SnLinkMetaImplCopyWithImpl(
|
||||
_$SnLinkMetaImpl _value, $Res Function(_$SnLinkMetaImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnLinkMeta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? entryId = null,
|
||||
Object? icon = freezed,
|
||||
Object? url = null,
|
||||
Object? title = freezed,
|
||||
Object? image = freezed,
|
||||
Object? video = freezed,
|
||||
Object? audio = freezed,
|
||||
Object? description = freezed,
|
||||
Object? siteName = freezed,
|
||||
Object? type = freezed,
|
||||
}) {
|
||||
return _then(_$SnLinkMetaImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
entryId: null == entryId
|
||||
? _value.entryId
|
||||
: entryId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: freezed == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
image: freezed == image
|
||||
? _value.image
|
||||
: image // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
audio: freezed == audio
|
||||
? _value.audio
|
||||
: audio // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
description: freezed == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
siteName: freezed == siteName
|
||||
? _value.siteName
|
||||
: siteName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
type: freezed == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnLinkMetaImpl extends _SnLinkMeta {
|
||||
const _$SnLinkMetaImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.entryId,
|
||||
required this.icon,
|
||||
required this.url,
|
||||
required this.title,
|
||||
required this.image,
|
||||
required this.video,
|
||||
required this.audio,
|
||||
required this.description,
|
||||
required this.siteName,
|
||||
required this.type})
|
||||
: super._();
|
||||
|
||||
factory _$SnLinkMetaImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnLinkMetaImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String entryId;
|
||||
@override
|
||||
final String? icon;
|
||||
@override
|
||||
final String url;
|
||||
@override
|
||||
final String? title;
|
||||
@override
|
||||
final String? image;
|
||||
@override
|
||||
final String? video;
|
||||
@override
|
||||
final String? audio;
|
||||
@override
|
||||
final String? description;
|
||||
@override
|
||||
final String? siteName;
|
||||
@override
|
||||
final String? type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)';
|
||||
}
|
||||
/// Serializes this SnLinkMeta to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnLinkMetaImpl &&
|
||||
other is SnLinkMeta &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
@ -375,76 +85,351 @@ class _$SnLinkMetaImpl extends _SnLinkMeta {
|
||||
siteName,
|
||||
type);
|
||||
|
||||
/// Create a copy of SnLinkMeta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith =>
|
||||
__$$SnLinkMetaImplCopyWithImpl<_$SnLinkMetaImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnLinkMetaImplToJson(
|
||||
this,
|
||||
);
|
||||
String toString() {
|
||||
return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnLinkMeta extends SnLinkMeta {
|
||||
const factory _SnLinkMeta(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final String entryId,
|
||||
required final String? icon,
|
||||
required final String url,
|
||||
required final String? title,
|
||||
required final String? image,
|
||||
required final String? video,
|
||||
required final String? audio,
|
||||
required final String? description,
|
||||
required final String? siteName,
|
||||
required final String? type}) = _$SnLinkMetaImpl;
|
||||
const _SnLinkMeta._() : super._();
|
||||
/// @nodoc
|
||||
abstract mixin class $SnLinkMetaCopyWith<$Res> {
|
||||
factory $SnLinkMetaCopyWith(
|
||||
SnLinkMeta value, $Res Function(SnLinkMeta) _then) =
|
||||
_$SnLinkMetaCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String entryId,
|
||||
String? icon,
|
||||
String url,
|
||||
String? title,
|
||||
String? image,
|
||||
String? video,
|
||||
String? audio,
|
||||
String? description,
|
||||
String? siteName,
|
||||
String? type});
|
||||
}
|
||||
|
||||
factory _SnLinkMeta.fromJson(Map<String, dynamic> json) =
|
||||
_$SnLinkMetaImpl.fromJson;
|
||||
/// @nodoc
|
||||
class _$SnLinkMetaCopyWithImpl<$Res> implements $SnLinkMetaCopyWith<$Res> {
|
||||
_$SnLinkMetaCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnLinkMeta _self;
|
||||
final $Res Function(SnLinkMeta) _then;
|
||||
|
||||
/// Create a copy of SnLinkMeta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? entryId = null,
|
||||
Object? icon = freezed,
|
||||
Object? url = null,
|
||||
Object? title = freezed,
|
||||
Object? image = freezed,
|
||||
Object? video = freezed,
|
||||
Object? audio = freezed,
|
||||
Object? description = freezed,
|
||||
Object? siteName = freezed,
|
||||
Object? type = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
entryId: null == entryId
|
||||
? _self.entryId
|
||||
: entryId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: freezed == icon
|
||||
? _self.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: null == url
|
||||
? _self.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: freezed == title
|
||||
? _self.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
image: freezed == image
|
||||
? _self.image
|
||||
: image // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
video: freezed == video
|
||||
? _self.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
audio: freezed == audio
|
||||
? _self.audio
|
||||
: audio // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
description: freezed == description
|
||||
? _self.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
siteName: freezed == siteName
|
||||
? _self.siteName
|
||||
: siteName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
type: freezed == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnLinkMeta extends SnLinkMeta {
|
||||
const _SnLinkMeta(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.entryId,
|
||||
required this.icon,
|
||||
required this.url,
|
||||
required this.title,
|
||||
required this.image,
|
||||
required this.video,
|
||||
required this.audio,
|
||||
required this.description,
|
||||
required this.siteName,
|
||||
required this.type})
|
||||
: super._();
|
||||
factory _SnLinkMeta.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnLinkMetaFromJson(json);
|
||||
|
||||
@override
|
||||
int get id;
|
||||
final int id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
String get entryId;
|
||||
final String entryId;
|
||||
@override
|
||||
String? get icon;
|
||||
final String? icon;
|
||||
@override
|
||||
String get url;
|
||||
final String url;
|
||||
@override
|
||||
String? get title;
|
||||
final String? title;
|
||||
@override
|
||||
String? get image;
|
||||
final String? image;
|
||||
@override
|
||||
String? get video;
|
||||
final String? video;
|
||||
@override
|
||||
String? get audio;
|
||||
final String? audio;
|
||||
@override
|
||||
String? get description;
|
||||
final String? description;
|
||||
@override
|
||||
String? get siteName;
|
||||
final String? siteName;
|
||||
@override
|
||||
String? get type;
|
||||
final String? type;
|
||||
|
||||
/// Create a copy of SnLinkMeta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnLinkMetaCopyWith<_SnLinkMeta> get copyWith =>
|
||||
__$SnLinkMetaCopyWithImpl<_SnLinkMeta>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnLinkMetaToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnLinkMeta &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.entryId, entryId) || other.entryId == entryId) &&
|
||||
(identical(other.icon, icon) || other.icon == icon) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.image, image) || other.image == image) &&
|
||||
(identical(other.video, video) || other.video == video) &&
|
||||
(identical(other.audio, audio) || other.audio == audio) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description) &&
|
||||
(identical(other.siteName, siteName) ||
|
||||
other.siteName == siteName) &&
|
||||
(identical(other.type, type) || other.type == type));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
entryId,
|
||||
icon,
|
||||
url,
|
||||
title,
|
||||
image,
|
||||
video,
|
||||
audio,
|
||||
description,
|
||||
siteName,
|
||||
type);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnLinkMetaCopyWith<$Res>
|
||||
implements $SnLinkMetaCopyWith<$Res> {
|
||||
factory _$SnLinkMetaCopyWith(
|
||||
_SnLinkMeta value, $Res Function(_SnLinkMeta) _then) =
|
||||
__$SnLinkMetaCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String entryId,
|
||||
String? icon,
|
||||
String url,
|
||||
String? title,
|
||||
String? image,
|
||||
String? video,
|
||||
String? audio,
|
||||
String? description,
|
||||
String? siteName,
|
||||
String? type});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnLinkMetaCopyWithImpl<$Res> implements _$SnLinkMetaCopyWith<$Res> {
|
||||
__$SnLinkMetaCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnLinkMeta _self;
|
||||
final $Res Function(_SnLinkMeta) _then;
|
||||
|
||||
/// Create a copy of SnLinkMeta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? entryId = null,
|
||||
Object? icon = freezed,
|
||||
Object? url = null,
|
||||
Object? title = freezed,
|
||||
Object? image = freezed,
|
||||
Object? video = freezed,
|
||||
Object? audio = freezed,
|
||||
Object? description = freezed,
|
||||
Object? siteName = freezed,
|
||||
Object? type = freezed,
|
||||
}) {
|
||||
return _then(_SnLinkMeta(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
entryId: null == entryId
|
||||
? _self.entryId
|
||||
: entryId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: freezed == icon
|
||||
? _self.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: null == url
|
||||
? _self.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: freezed == title
|
||||
? _self.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
image: freezed == image
|
||||
? _self.image
|
||||
: image // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
video: freezed == video
|
||||
? _self.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
audio: freezed == audio
|
||||
? _self.audio
|
||||
: audio // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
description: freezed == description
|
||||
? _self.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
siteName: freezed == siteName
|
||||
? _self.siteName
|
||||
: siteName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
type: freezed == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@ -6,8 +6,7 @@ part of 'link.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnLinkMetaImpl _$$SnLinkMetaImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnLinkMetaImpl(
|
||||
_SnLinkMeta _$SnLinkMetaFromJson(Map<String, dynamic> json) => _SnLinkMeta(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -26,7 +25,7 @@ _$SnLinkMetaImpl _$$SnLinkMetaImplFromJson(Map<String, dynamic> json) =>
|
||||
type: json['type'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnLinkMetaImplToJson(_$SnLinkMetaImpl instance) =>
|
||||
Map<String, dynamic> _$SnLinkMetaToJson(_SnLinkMeta instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@ -4,7 +4,7 @@ part 'news.freezed.dart';
|
||||
part 'news.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnNewsSource with _$SnNewsSource {
|
||||
abstract class SnNewsSource with _$SnNewsSource {
|
||||
const factory SnNewsSource({
|
||||
required String id,
|
||||
required String label,
|
||||
@ -18,7 +18,7 @@ class SnNewsSource with _$SnNewsSource {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnNewsArticle with _$SnNewsArticle {
|
||||
abstract class SnNewsArticle with _$SnNewsArticle {
|
||||
const factory SnNewsArticle({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,8 @@ part of 'news.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsSourceImpl(
|
||||
_SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) =>
|
||||
_SnNewsSource(
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String,
|
||||
type: json['type'] as String,
|
||||
@ -16,7 +16,7 @@ _$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
|
||||
enabled: json['enabled'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
|
||||
Map<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
@ -26,8 +26,8 @@ Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
|
||||
'enabled': instance.enabled,
|
||||
};
|
||||
|
||||
_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsArticleImpl(
|
||||
_SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) =>
|
||||
_SnNewsArticle(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -44,7 +44,7 @@ _$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
|
||||
Map<String, dynamic> _$SnNewsArticleToJson(_SnNewsArticle instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@ -4,7 +4,7 @@ part 'notification.freezed.dart';
|
||||
part 'notification.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnNotification with _$SnNotification {
|
||||
abstract class SnNotification with _$SnNotification {
|
||||
const factory SnNotification({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
|
@ -1,3 +1,4 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
@ -9,156 +10,92 @@ part of 'notification.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) {
|
||||
return _SnNotification.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnNotification {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
String get topic => throw _privateConstructorUsedError;
|
||||
String get title => throw _privateConstructorUsedError;
|
||||
String? get subtitle => throw _privateConstructorUsedError;
|
||||
String get body => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get metadata => throw _privateConstructorUsedError;
|
||||
int get priority => throw _privateConstructorUsedError;
|
||||
int? get senderId => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
DateTime? get readAt => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnNotification to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get topic;
|
||||
String get title;
|
||||
String? get subtitle;
|
||||
String get body;
|
||||
Map<String, dynamic> get metadata;
|
||||
int get priority;
|
||||
int? get senderId;
|
||||
int get accountId;
|
||||
DateTime? get readAt;
|
||||
|
||||
/// Create a copy of SnNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnNotificationCopyWith<SnNotification> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnNotificationCopyWith<$Res> {
|
||||
factory $SnNotificationCopyWith(
|
||||
SnNotification value, $Res Function(SnNotification) then) =
|
||||
_$SnNotificationCopyWithImpl<$Res, SnNotification>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String topic,
|
||||
String title,
|
||||
String? subtitle,
|
||||
String body,
|
||||
Map<String, dynamic> metadata,
|
||||
int priority,
|
||||
int? senderId,
|
||||
int accountId,
|
||||
DateTime? readAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnNotificationCopyWithImpl<$Res, $Val extends SnNotification>
|
||||
implements $SnNotificationCopyWith<$Res> {
|
||||
_$SnNotificationCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnNotificationCopyWith<SnNotification> get copyWith =>
|
||||
_$SnNotificationCopyWithImpl<SnNotification>(
|
||||
this as SnNotification, _$identity);
|
||||
|
||||
/// Serializes this SnNotification to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? topic = null,
|
||||
Object? title = null,
|
||||
Object? subtitle = freezed,
|
||||
Object? body = null,
|
||||
Object? metadata = null,
|
||||
Object? priority = null,
|
||||
Object? senderId = freezed,
|
||||
Object? accountId = null,
|
||||
Object? readAt = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
topic: null == topic
|
||||
? _value.topic
|
||||
: topic // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
subtitle: freezed == subtitle
|
||||
? _value.subtitle
|
||||
: subtitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
body: null == body
|
||||
? _value.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
metadata: null == metadata
|
||||
? _value.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
priority: null == priority
|
||||
? _value.priority
|
||||
: priority // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
senderId: freezed == senderId
|
||||
? _value.senderId
|
||||
: senderId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
readAt: freezed == readAt
|
||||
? _value.readAt
|
||||
: readAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
) as $Val);
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnNotification &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.topic, topic) || other.topic == topic) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.subtitle, subtitle) ||
|
||||
other.subtitle == subtitle) &&
|
||||
(identical(other.body, body) || other.body == body) &&
|
||||
const DeepCollectionEquality().equals(other.metadata, metadata) &&
|
||||
(identical(other.priority, priority) ||
|
||||
other.priority == priority) &&
|
||||
(identical(other.senderId, senderId) ||
|
||||
other.senderId == senderId) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.readAt, readAt) || other.readAt == readAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
topic,
|
||||
title,
|
||||
subtitle,
|
||||
body,
|
||||
const DeepCollectionEquality().hash(metadata),
|
||||
priority,
|
||||
senderId,
|
||||
accountId,
|
||||
readAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnNotification(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, topic: $topic, title: $title, subtitle: $subtitle, body: $body, metadata: $metadata, priority: $priority, senderId: $senderId, accountId: $accountId, readAt: $readAt)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnNotificationImplCopyWith<$Res>
|
||||
implements $SnNotificationCopyWith<$Res> {
|
||||
factory _$$SnNotificationImplCopyWith(_$SnNotificationImpl value,
|
||||
$Res Function(_$SnNotificationImpl) then) =
|
||||
__$$SnNotificationImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
abstract mixin class $SnNotificationCopyWith<$Res> {
|
||||
factory $SnNotificationCopyWith(
|
||||
SnNotification value, $Res Function(SnNotification) _then) =
|
||||
_$SnNotificationCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
@ -177,12 +114,12 @@ abstract class _$$SnNotificationImplCopyWith<$Res>
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnNotificationImplCopyWithImpl<$Res>
|
||||
extends _$SnNotificationCopyWithImpl<$Res, _$SnNotificationImpl>
|
||||
implements _$$SnNotificationImplCopyWith<$Res> {
|
||||
__$$SnNotificationImplCopyWithImpl(
|
||||
_$SnNotificationImpl _value, $Res Function(_$SnNotificationImpl) _then)
|
||||
: super(_value, _then);
|
||||
class _$SnNotificationCopyWithImpl<$Res>
|
||||
implements $SnNotificationCopyWith<$Res> {
|
||||
_$SnNotificationCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnNotification _self;
|
||||
final $Res Function(SnNotification) _then;
|
||||
|
||||
/// Create a copy of SnNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -203,57 +140,57 @@ class __$$SnNotificationImplCopyWithImpl<$Res>
|
||||
Object? accountId = null,
|
||||
Object? readAt = freezed,
|
||||
}) {
|
||||
return _then(_$SnNotificationImpl(
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
topic: null == topic
|
||||
? _value.topic
|
||||
? _self.topic
|
||||
: topic // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
? _self.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
subtitle: freezed == subtitle
|
||||
? _value.subtitle
|
||||
? _self.subtitle
|
||||
: subtitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
body: null == body
|
||||
? _value.body
|
||||
? _self.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
metadata: null == metadata
|
||||
? _value._metadata
|
||||
? _self.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
priority: null == priority
|
||||
? _value.priority
|
||||
? _self.priority
|
||||
: priority // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
senderId: freezed == senderId
|
||||
? _value.senderId
|
||||
? _self.senderId
|
||||
: senderId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
readAt: freezed == readAt
|
||||
? _value.readAt
|
||||
? _self.readAt
|
||||
: readAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
@ -262,8 +199,8 @@ class __$$SnNotificationImplCopyWithImpl<$Res>
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnNotificationImpl implements _SnNotification {
|
||||
const _$SnNotificationImpl(
|
||||
class _SnNotification implements SnNotification {
|
||||
const _SnNotification(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@ -278,9 +215,8 @@ class _$SnNotificationImpl implements _SnNotification {
|
||||
required this.accountId,
|
||||
required this.readAt})
|
||||
: _metadata = metadata;
|
||||
|
||||
factory _$SnNotificationImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnNotificationImplFromJson(json);
|
||||
factory _SnNotification.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnNotificationFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@ -316,16 +252,26 @@ class _$SnNotificationImpl implements _SnNotification {
|
||||
@override
|
||||
final DateTime? readAt;
|
||||
|
||||
/// Create a copy of SnNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnNotification(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, topic: $topic, title: $title, subtitle: $subtitle, body: $body, metadata: $metadata, priority: $priority, senderId: $senderId, accountId: $accountId, readAt: $readAt)';
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnNotificationCopyWith<_SnNotification> get copyWith =>
|
||||
__$SnNotificationCopyWithImpl<_SnNotification>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnNotificationToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnNotificationImpl &&
|
||||
other is _SnNotification &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
@ -366,73 +312,118 @@ class _$SnNotificationImpl implements _SnNotification {
|
||||
accountId,
|
||||
readAt);
|
||||
|
||||
/// Create a copy of SnNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnNotificationImplCopyWith<_$SnNotificationImpl> get copyWith =>
|
||||
__$$SnNotificationImplCopyWithImpl<_$SnNotificationImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnNotificationImplToJson(
|
||||
this,
|
||||
);
|
||||
String toString() {
|
||||
return 'SnNotification(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, topic: $topic, title: $title, subtitle: $subtitle, body: $body, metadata: $metadata, priority: $priority, senderId: $senderId, accountId: $accountId, readAt: $readAt)';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnNotification implements SnNotification {
|
||||
const factory _SnNotification(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final String topic,
|
||||
required final String title,
|
||||
required final String? subtitle,
|
||||
required final String body,
|
||||
final Map<String, dynamic> metadata,
|
||||
required final int priority,
|
||||
required final int? senderId,
|
||||
required final int accountId,
|
||||
required final DateTime? readAt}) = _$SnNotificationImpl;
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnNotificationCopyWith<$Res>
|
||||
implements $SnNotificationCopyWith<$Res> {
|
||||
factory _$SnNotificationCopyWith(
|
||||
_SnNotification value, $Res Function(_SnNotification) _then) =
|
||||
__$SnNotificationCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String topic,
|
||||
String title,
|
||||
String? subtitle,
|
||||
String body,
|
||||
Map<String, dynamic> metadata,
|
||||
int priority,
|
||||
int? senderId,
|
||||
int accountId,
|
||||
DateTime? readAt});
|
||||
}
|
||||
|
||||
factory _SnNotification.fromJson(Map<String, dynamic> json) =
|
||||
_$SnNotificationImpl.fromJson;
|
||||
/// @nodoc
|
||||
class __$SnNotificationCopyWithImpl<$Res>
|
||||
implements _$SnNotificationCopyWith<$Res> {
|
||||
__$SnNotificationCopyWithImpl(this._self, this._then);
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
@override
|
||||
String get topic;
|
||||
@override
|
||||
String get title;
|
||||
@override
|
||||
String? get subtitle;
|
||||
@override
|
||||
String get body;
|
||||
@override
|
||||
Map<String, dynamic> get metadata;
|
||||
@override
|
||||
int get priority;
|
||||
@override
|
||||
int? get senderId;
|
||||
@override
|
||||
int get accountId;
|
||||
@override
|
||||
DateTime? get readAt;
|
||||
final _SnNotification _self;
|
||||
final $Res Function(_SnNotification) _then;
|
||||
|
||||
/// Create a copy of SnNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnNotificationImplCopyWith<_$SnNotificationImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? topic = null,
|
||||
Object? title = null,
|
||||
Object? subtitle = freezed,
|
||||
Object? body = null,
|
||||
Object? metadata = null,
|
||||
Object? priority = null,
|
||||
Object? senderId = freezed,
|
||||
Object? accountId = null,
|
||||
Object? readAt = freezed,
|
||||
}) {
|
||||
return _then(_SnNotification(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
topic: null == topic
|
||||
? _self.topic
|
||||
: topic // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _self.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
subtitle: freezed == subtitle
|
||||
? _self.subtitle
|
||||
: subtitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
body: null == body
|
||||
? _self.body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
metadata: null == metadata
|
||||
? _self._metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
priority: null == priority
|
||||
? _self.priority
|
||||
: priority // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
senderId: freezed == senderId
|
||||
? _self.senderId
|
||||
: senderId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
readAt: freezed == readAt
|
||||
? _self.readAt
|
||||
: readAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@ -6,8 +6,8 @@ part of 'notification.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnNotificationImpl _$$SnNotificationImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnNotificationImpl(
|
||||
_SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) =>
|
||||
_SnNotification(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -27,8 +27,7 @@ _$SnNotificationImpl _$$SnNotificationImplFromJson(Map<String, dynamic> json) =>
|
||||
: DateTime.parse(json['read_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnNotificationImplToJson(
|
||||
_$SnNotificationImpl instance) =>
|
||||
Map<String, dynamic> _$SnNotificationToJson(_SnNotification instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@ -4,7 +4,7 @@ part 'poll.freezed.dart';
|
||||
part 'poll.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnPoll with _$SnPoll {
|
||||
abstract class SnPoll with _$SnPoll {
|
||||
const factory SnPoll({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -20,7 +20,7 @@ class SnPoll with _$SnPoll {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollMetric with _$SnPollMetric {
|
||||
abstract class SnPollMetric with _$SnPollMetric {
|
||||
const factory SnPollMetric({
|
||||
required int totalAnswer,
|
||||
@Default({}) Map<String, int> byOptions,
|
||||
@ -32,7 +32,7 @@ class SnPollMetric with _$SnPollMetric {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollOption with _$SnPollOption {
|
||||
abstract class SnPollOption with _$SnPollOption {
|
||||
const factory SnPollOption({
|
||||
required String id,
|
||||
required String icon,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ part of 'poll.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnPollImpl _$$SnPollImplFromJson(Map<String, dynamic> json) => _$SnPollImpl(
|
||||
_SnPoll _$SnPollFromJson(Map<String, dynamic> json) => _SnPoll(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -19,8 +19,7 @@ _$SnPollImpl _$$SnPollImplFromJson(Map<String, dynamic> json) => _$SnPollImpl(
|
||||
metric: SnPollMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$SnPollToJson(_SnPoll instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
@ -31,8 +30,8 @@ Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) =>
|
||||
'metric': instance.metric.toJson(),
|
||||
};
|
||||
|
||||
_$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollMetricImpl(
|
||||
_SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) =>
|
||||
_SnPollMetric(
|
||||
totalAnswer: (json['total_answer'] as num).toInt(),
|
||||
byOptions: (json['by_options'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
@ -45,22 +44,22 @@ _$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) =>
|
||||
Map<String, dynamic> _$SnPollMetricToJson(_SnPollMetric instance) =>
|
||||
<String, dynamic>{
|
||||
'total_answer': instance.totalAnswer,
|
||||
'by_options': instance.byOptions,
|
||||
'by_options_percentage': instance.byOptionsPercentage,
|
||||
};
|
||||
|
||||
_$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollOptionImpl(
|
||||
_SnPollOption _$SnPollOptionFromJson(Map<String, dynamic> json) =>
|
||||
_SnPollOption(
|
||||
id: json['id'] as String,
|
||||
icon: json['icon'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollOptionImplToJson(_$SnPollOptionImpl instance) =>
|
||||
Map<String, dynamic> _$SnPollOptionToJson(_SnPollOption instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'icon': instance.icon,
|
||||
|
@ -7,7 +7,7 @@ part 'post.freezed.dart';
|
||||
part 'post.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnPost with _$SnPost {
|
||||
abstract class SnPost with _$SnPost {
|
||||
const SnPost._();
|
||||
|
||||
const factory SnPost({
|
||||
@ -57,7 +57,7 @@ class SnPost with _$SnPost {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPostTag with _$SnPostTag {
|
||||
abstract class SnPostTag with _$SnPostTag {
|
||||
const factory SnPostTag({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -74,7 +74,7 @@ class SnPostTag with _$SnPostTag {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPostCategory with _$SnPostCategory {
|
||||
abstract class SnPostCategory with _$SnPostCategory {
|
||||
const factory SnPostCategory({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -91,7 +91,7 @@ class SnPostCategory with _$SnPostCategory {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPostPreload with _$SnPostPreload {
|
||||
abstract class SnPostPreload with _$SnPostPreload {
|
||||
const factory SnPostPreload({
|
||||
required SnAttachment? thumbnail,
|
||||
required List<SnAttachment?>? attachments,
|
||||
@ -105,7 +105,7 @@ class SnPostPreload with _$SnPostPreload {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnBody with _$SnBody {
|
||||
abstract class SnBody with _$SnBody {
|
||||
const factory SnBody({
|
||||
required List<String> attachments,
|
||||
required String content,
|
||||
@ -118,7 +118,7 @@ class SnBody with _$SnBody {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnMetric with _$SnMetric {
|
||||
abstract class SnMetric with _$SnMetric {
|
||||
const factory SnMetric({
|
||||
required int replyCount,
|
||||
required int reactionCount,
|
||||
@ -130,7 +130,7 @@ class SnMetric with _$SnMetric {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPublisher with _$SnPublisher {
|
||||
abstract class SnPublisher with _$SnPublisher {
|
||||
const factory SnPublisher({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -153,7 +153,7 @@ class SnPublisher with _$SnPublisher {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnSubscription with _$SnSubscription {
|
||||
abstract class SnSubscription with _$SnSubscription {
|
||||
const factory SnSubscription({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ part of 'post.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
_SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -76,8 +76,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
: SnPostPreload.fromJson(json['preload'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
@ -115,8 +114,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'preload': instance.preload?.toJson(),
|
||||
};
|
||||
|
||||
_$SnPostTagImpl _$$SnPostTagImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostTagImpl(
|
||||
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -127,7 +125,7 @@ _$SnPostTagImpl _$$SnPostTagImplFromJson(Map<String, dynamic> json) =>
|
||||
posts: json['posts'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostTagImplToJson(_$SnPostTagImpl instance) =>
|
||||
Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -139,8 +137,8 @@ Map<String, dynamic> _$$SnPostTagImplToJson(_$SnPostTagImpl instance) =>
|
||||
'posts': instance.posts,
|
||||
};
|
||||
|
||||
_$SnPostCategoryImpl _$$SnPostCategoryImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostCategoryImpl(
|
||||
_SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) =>
|
||||
_SnPostCategory(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -151,8 +149,7 @@ _$SnPostCategoryImpl _$$SnPostCategoryImplFromJson(Map<String, dynamic> json) =>
|
||||
posts: json['posts'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostCategoryImplToJson(
|
||||
_$SnPostCategoryImpl instance) =>
|
||||
Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -164,8 +161,8 @@ Map<String, dynamic> _$$SnPostCategoryImplToJson(
|
||||
'posts': instance.posts,
|
||||
};
|
||||
|
||||
_$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostPreloadImpl(
|
||||
_SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) =>
|
||||
_SnPostPreload(
|
||||
thumbnail: json['thumbnail'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['thumbnail'] as Map<String, dynamic>),
|
||||
@ -185,7 +182,7 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
||||
Map<String, dynamic> _$SnPostPreloadToJson(_SnPostPreload instance) =>
|
||||
<String, dynamic>{
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||
@ -194,7 +191,7 @@ Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
||||
'realm': instance.realm?.toJson(),
|
||||
};
|
||||
|
||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
||||
_SnBody _$SnBodyFromJson(Map<String, dynamic> json) => _SnBody(
|
||||
attachments: (json['attachments'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
@ -204,8 +201,7 @@ _$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
||||
title: json['title'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnBodyImplToJson(_$SnBodyImpl instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$SnBodyToJson(_SnBody instance) => <String, dynamic>{
|
||||
'attachments': instance.attachments,
|
||||
'content': instance.content,
|
||||
'location': instance.location,
|
||||
@ -213,8 +209,7 @@ Map<String, dynamic> _$$SnBodyImplToJson(_$SnBodyImpl instance) =>
|
||||
'title': instance.title,
|
||||
};
|
||||
|
||||
_$SnMetricImpl _$$SnMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnMetricImpl(
|
||||
_SnMetric _$SnMetricFromJson(Map<String, dynamic> json) => _SnMetric(
|
||||
replyCount: (json['reply_count'] as num).toInt(),
|
||||
reactionCount: (json['reaction_count'] as num).toInt(),
|
||||
reactionList: (json['reaction_list'] as Map<String, dynamic>?)?.map(
|
||||
@ -223,15 +218,13 @@ _$SnMetricImpl _$$SnMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnMetricImplToJson(_$SnMetricImpl instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$SnMetricToJson(_SnMetric instance) => <String, dynamic>{
|
||||
'reply_count': instance.replyCount,
|
||||
'reaction_count': instance.reactionCount,
|
||||
'reaction_list': instance.reactionList,
|
||||
};
|
||||
|
||||
_$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPublisherImpl(
|
||||
_SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -250,7 +243,7 @@ _$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) =>
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPublisherImplToJson(_$SnPublisherImpl instance) =>
|
||||
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -268,8 +261,8 @@ Map<String, dynamic> _$$SnPublisherImplToJson(_$SnPublisherImpl instance) =>
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnSubscriptionImpl _$$SnSubscriptionImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnSubscriptionImpl(
|
||||
_SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) =>
|
||||
_SnSubscription(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -280,8 +273,7 @@ _$SnSubscriptionImpl _$$SnSubscriptionImplFromJson(Map<String, dynamic> json) =>
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnSubscriptionImplToJson(
|
||||
_$SnSubscriptionImpl instance) =>
|
||||
Map<String, dynamic> _$SnSubscriptionToJson(_SnSubscription instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@ -5,7 +5,7 @@ part 'realm.freezed.dart';
|
||||
part 'realm.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnRealmMember with _$SnRealmMember {
|
||||
abstract class SnRealmMember with _$SnRealmMember {
|
||||
const factory SnRealmMember({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -23,7 +23,7 @@ class SnRealmMember with _$SnRealmMember {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnRealm with _$SnRealm {
|
||||
abstract class SnRealm with _$SnRealm {
|
||||
const SnRealm._();
|
||||
|
||||
const factory SnRealm({
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,8 @@ part of 'realm.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnRealmMemberImpl _$$SnRealmMemberImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnRealmMemberImpl(
|
||||
_SnRealmMember _$SnRealmMemberFromJson(Map<String, dynamic> json) =>
|
||||
_SnRealmMember(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -21,7 +21,7 @@ _$SnRealmMemberImpl _$$SnRealmMemberImplFromJson(Map<String, dynamic> json) =>
|
||||
powerLevel: (json['power_level'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnRealmMemberImplToJson(_$SnRealmMemberImpl instance) =>
|
||||
Map<String, dynamic> _$SnRealmMemberToJson(_SnRealmMember instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
@ -34,8 +34,7 @@ Map<String, dynamic> _$$SnRealmMemberImplToJson(_$SnRealmMemberImpl instance) =>
|
||||
'power_level': instance.powerLevel,
|
||||
};
|
||||
|
||||
_$SnRealmImpl _$$SnRealmImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnRealmImpl(
|
||||
_SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
@ -57,8 +56,7 @@ _$SnRealmImpl _$$SnRealmImplFromJson(Map<String, dynamic> json) =>
|
||||
popularity: (json['popularity'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnRealmImplToJson(_$SnRealmImpl instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$SnRealmToJson(_SnRealm instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
|
@ -4,7 +4,7 @@ part 'wallet.freezed.dart';
|
||||
part 'wallet.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnWallet with _$SnWallet {
|
||||
abstract class SnWallet with _$SnWallet {
|
||||
const factory SnWallet({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
@ -19,7 +19,7 @@ class SnWallet with _$SnWallet {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnTransaction with _$SnTransaction {
|
||||
abstract class SnTransaction with _$SnTransaction {
|
||||
const factory SnTransaction({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user