Compare commits
82 Commits
3.2.0+132
...
54560ad5d8
Author | SHA1 | Date | |
---|---|---|---|
54560ad5d8
|
|||
0c729db639
|
|||
1fbaac8d88
|
|||
b9dc724f0b
|
|||
a2cc55696f
|
|||
e79f857feb
|
|||
affba29c04
|
|||
756746b144
|
|||
28b6eade48
|
|||
1de7ef8c96 | |||
67eac5dcf5 | |||
7a44bfa075
|
|||
1c2f25a152
|
|||
be26ea280e
|
|||
b4996d069f
|
|||
bf4892b34d
|
|||
5f84751fd5
|
|||
457d1bac60
|
|||
02ec11845b
|
|||
612f1bf004
|
|||
fd80b713ad
|
|||
508805368c
|
|||
98eb28a4ec
|
|||
d1a2f59dd1
|
|||
bb9adb963a
|
|||
83e40cd860
|
|||
c06fb12f6a
|
|||
6600cf4df8
|
|||
4293daaa2f
|
|||
866674ddde
|
|||
27d478ba4f
|
|||
cccade763f
|
|||
f760b85186
|
|||
e68c5f4f92
|
|||
b0f3b6b5c3
|
|||
cb2af379fa
|
|||
38f8103265
|
|||
06bb18bdaa
|
|||
84c38500d0
|
|||
9529bbf08b
|
|||
8baf77bcf7
|
|||
b2ac5fbef2
|
|||
c79b1d7aab
|
|||
|
4f55a8209c | ||
|
ace302111a | ||
|
1391fa0dde | ||
|
cbdc7acdcd | ||
|
b80d91825a | ||
|
1a703b7eba | ||
|
3621ea7744 | ||
|
b638343f02 | ||
|
269a64cabb | ||
406e5187a8
|
|||
9bdd08d8dd
|
|||
d737232dcf
|
|||
c9d751479e
|
|||
a2c2bfe585
|
|||
c7f9da0dee
|
|||
|
a243cda1df | ||
|
7b238f32fd | ||
313af28d7f
|
|||
c64e1e208c
|
|||
c9b07a9a2a
|
|||
55c0e355f1 | |||
be414891ec | |||
787876ab6a | |||
8578cde620
|
|||
14d55d45a8
|
|||
724391584e
|
|||
3a5e45808a | |||
488055955c
|
|||
|
313ebc64cc | ||
|
1ed8b1d0c1 | ||
4af816d931 | |||
1c058a4323 | |||
461ed1fcda | |||
5363afa558
|
|||
f0d2737da8
|
|||
1f2f80aa3e
|
|||
240a872e65
|
|||
c1ec6f0849 | |||
ab42686d4d |
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/es-ES.json
Normal file
1079
assets/i18n/es-ES.json
Normal file
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/ja-JP.json
Normal file
1079
assets/i18n/ja-JP.json
Normal file
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/ko-KR.json
Normal file
1079
assets/i18n/ko-KR.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/zh-OG.json
Normal file
1079
assets/i18n/zh-OG.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -303,8 +303,7 @@
|
||||
"notifications": "通知",
|
||||
"posts": "帖子",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||
"settingsBackgroundImageClear": "清除背景圖片",
|
||||
"settingsBackgroundImageClear": "清除背景圖片",
|
||||
"settingsBackgroundGenerateColor": "從背景圖像生成主題色",
|
||||
"messageNone": "沒有內容可顯示",
|
||||
"unreadMessages": {
|
||||
@@ -315,8 +314,6 @@
|
||||
"settingsRealmCompactView": "緊湊領域視圖",
|
||||
"settingsMixedFeed": "混合動態",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"settingsHideBottomNav": "隱藏底部導航",
|
||||
"settingsSoundEffects": "音效",
|
||||
"settingsAprilFoolFeatures": "愚人節功能",
|
||||
@@ -671,7 +668,6 @@
|
||||
"publisherFeatureDevelopDescription": "為你的開發者解鎖包括應用套件,API 及更多開發功能。",
|
||||
"publisherFeatureDevelopHint": "目前該功能還在開發中,你需要邀請才可解鎖。",
|
||||
"learnMore": "瞭解更多",
|
||||
"discoverWebArticles": "來自站外的文章",
|
||||
"webArticlesStand": "文章亭",
|
||||
"about": "關於",
|
||||
"somethingWentWrong": "發生了一些錯誤",
|
||||
@@ -694,8 +690,6 @@
|
||||
"sharePostPhoto": "通過圖片分享帖子",
|
||||
"wouldYouLikeToNavigateToChat": "你想要前往聊天頁面嗎?",
|
||||
"abuseReports": "舉報",
|
||||
"discoverRealms": "發現領域",
|
||||
"discoverPublishers": "發現發佈者",
|
||||
"membershipCancel": "取消會員訂閱",
|
||||
"membershipCancelConfirm": "你確定要取消會員訂閱嗎?",
|
||||
"membershipCancelHint": "你確定要取消會員訂閱嗎?你將不會再次被扣費。你的會員資格將在當前計費週期結束前保持有效。並且你將無法重新訂閱,直到當前訂閱結束。",
|
||||
@@ -759,19 +753,19 @@
|
||||
"markAsSensitive": "標記為敏感",
|
||||
"fileName": "文件名",
|
||||
"sensitiveCategories": {
|
||||
"language": "語言",
|
||||
"sexualContent": "色情內容",
|
||||
"violence": "暴力",
|
||||
"profanity": "褻瀆",
|
||||
"hateSpeech": "仇恨言論",
|
||||
"racism": "種族主義",
|
||||
"adultContent": "成人內容",
|
||||
"drugAbuse": "藥物濫用",
|
||||
"alcoholAbuse": "酗酒",
|
||||
"gambling": "賭博",
|
||||
"selfHarm": "自殘",
|
||||
"childAbuse": "虐待兒童",
|
||||
"other": "其他"
|
||||
"language": "Language",
|
||||
"sexualContent": "Sexual Content",
|
||||
"violence": "Violence",
|
||||
"profanity": "Profanity",
|
||||
"hateSpeech": "Hate Speech",
|
||||
"racism": "Racism",
|
||||
"adultContent": "Adult Content",
|
||||
"drugAbuse": "Drug Abuse",
|
||||
"alcoholAbuse": "Alcohol Abuse",
|
||||
"gambling": "Gambling",
|
||||
"selfHarm": "Self-harm",
|
||||
"childAbuse": "Child Abuse",
|
||||
"other": "Other"
|
||||
},
|
||||
"poll": "投票",
|
||||
"pollsRecent": "最近投票",
|
||||
@@ -815,6 +809,159 @@
|
||||
"one": "+{} 個文件被摺疊",
|
||||
"other": "+{} 個文件被摺疊"
|
||||
},
|
||||
"pollQuestions": "Questions",
|
||||
"pollAnswerSubmitted": "Poll answer has been submitted.",
|
||||
"modifyAnswers": "Modify Answers",
|
||||
"back": "Back",
|
||||
"submit": "Submit",
|
||||
"pollOptionDefaultLabel": "Option 1",
|
||||
"pollUpdated": "Poll updated.",
|
||||
"pollCreated": "Poll created.",
|
||||
"pollCreate": "Create Poll",
|
||||
"pollEdit": "Edit Poll",
|
||||
"pollPreviewJsonDebug": "Debug Preview",
|
||||
"pollTitleRequired": "Title is required",
|
||||
"pollEndDateOptional": "End date & time (optional)",
|
||||
"notSet": "Not set",
|
||||
"pick": "Pick",
|
||||
"clear": "Clear",
|
||||
"questions": "Questions",
|
||||
"pollAddQuestion": "Add question",
|
||||
"pollQuestionTypeSingleChoice": "Single choice",
|
||||
"pollQuestionTypeMultipleChoice": "Multiple choice",
|
||||
"pollQuestionTypeFreeText": "Free text",
|
||||
"pollQuestionTypeYesNo": "Yes / No",
|
||||
"pollQuestionTypeRating": "Rating",
|
||||
"pollNoQuestionsYet": "No questions yet",
|
||||
"pollNoQuestionsHint": "Use \"Add question\" to start building your poll.",
|
||||
"pollDebugPreview": "Debug Preview",
|
||||
"pollUntitledQuestion": "Untitled question",
|
||||
"moveUp": "Move up",
|
||||
"moveDown": "Move down",
|
||||
"required": "Required",
|
||||
"pollQuestionTitle": "Question title",
|
||||
"pollQuestionTitleRequired": "Question title is required",
|
||||
"pollQuestionDescriptionOptional": "Question description (optional)",
|
||||
"options": "Options",
|
||||
"pollAddOption": "Add option",
|
||||
"pollOptionLabel": "Option label",
|
||||
"pollLongTextAnswerPreview": "Long text answer (preview)",
|
||||
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
||||
"award": "Award",
|
||||
"awardPost": "Award Post",
|
||||
"awardMessage": "Message",
|
||||
"awardMessageHint": "Enter your award message...",
|
||||
"awardAttitude": "Attitude",
|
||||
"awardAttitudePositive": "Positive",
|
||||
"awardAttitudeNegative": "Negative",
|
||||
"awardAmount": "Amount",
|
||||
"awardAmountHint": "Enter amount...",
|
||||
"awardAmountRequired": "Amount is required",
|
||||
"awardAmountInvalid": "Please enter a valid amount",
|
||||
"awardMessageTooLong": "Message is too long (max 4096 characters)",
|
||||
"awardSuccess": "Award sent successfully!",
|
||||
"awardSubmit": "Award",
|
||||
"awardPostPreview": "Post Preview",
|
||||
"awardNoContent": "No content available",
|
||||
"awardByPublisher": "By {}",
|
||||
"awardBenefits": "Award Benefits",
|
||||
"awardBenefitsDescription": "Awarding this post increases its value and visibility. Higher valued posts have a better chance of being featured and highlighted in the community.",
|
||||
"checkInResultLevel5": "Happy Birthday 🥳",
|
||||
"region": "Region",
|
||||
"accountRegionHint": "This region will be used for content delivery and localization.",
|
||||
"settingsCustomFontsHelper": "Use comma to seprate.",
|
||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"postTypePost": "Post",
|
||||
"searchDrafts": "Search drafts...",
|
||||
"noSearchResults": "No search results",
|
||||
"contactMethodMakePublic": "Make Public",
|
||||
"contactMethodMakePrivate": "Make Private",
|
||||
"contactMethodPublic": "Public",
|
||||
"contactMethodPrivate": "Private",
|
||||
"discoverRealms": "發現領域",
|
||||
"discoverPublishers": "發現發佈者",
|
||||
"discoverShuffledPost": "Random Posts",
|
||||
"projects": "Projects",
|
||||
"noProjects": "No projects found.",
|
||||
"deleteProject": "Delete Project",
|
||||
"deleteProjectHint": "Are you sure you want to delete this project? This action cannot be undone.",
|
||||
"createProject": "Create Project",
|
||||
"editProject": "Edit Project",
|
||||
"projectDetails": "Project Details",
|
||||
"createBot": "Create Bot",
|
||||
"bots": "Bots",
|
||||
"noBots": "No bots yet.",
|
||||
"deleteBotHint": "Are you sure you want to delete this bot? This action cannot be undone.",
|
||||
"deleteBot": "Delete Bot",
|
||||
"discoverWebArticles": "來自站外的文章",
|
||||
"messageJumpNotLoaded": "The referenced message was not loaded, unable to jump to it.",
|
||||
"postUnlinkRealm": "No linked realm",
|
||||
"postSlug": "Slug",
|
||||
"postSlugHint": "The slug can be used to access your post via URL in the webpage, it should be publisher-wide unique.",
|
||||
"attachmentOnDevice": "On-device",
|
||||
"attachmentOnCloud": "On-cloud",
|
||||
"attachments": "Attachments",
|
||||
"publisherCollabInvitation": "Collabration invitations",
|
||||
"publisherCollabInvitationCount": {
|
||||
"zero": "No invitation",
|
||||
"one": "{} available invitation",
|
||||
"other": "{} available invitations"
|
||||
},
|
||||
"failedToLoadUserInfo": "Failed to load user info",
|
||||
"failedToLoadUserInfoNetwork": "It seems be network issue, you can tap the button below to try again.",
|
||||
"failedToLoadUserInfoUnauthorized": "It seems your session has been logged out or not available anymore, you can still try agian to fetch the user info if you want.",
|
||||
"okay": "Okay",
|
||||
"postDetail": "Post Detail",
|
||||
"postCount": {
|
||||
"zero": "No posts",
|
||||
"one": "{} post",
|
||||
"other": "{} posts"
|
||||
},
|
||||
"mimeType": "MIME Type",
|
||||
"fileSize": "File Size",
|
||||
"fileHash": "File Hash",
|
||||
"exifData": "EXIF Data",
|
||||
"postShuffle": "Shuffle Posts",
|
||||
"leveling": "Leveling",
|
||||
"levelingHistory": "Leveling History",
|
||||
"stellarProgram": "Stellar Program",
|
||||
"socialCredits": "Social Credits",
|
||||
"credits": "Credits",
|
||||
"creditsStatus": "Credits Status",
|
||||
"socialCreditsDescription": "Social Credit is a way for Solar Network to evaluate users. It is calculated based on their behavior and interactions. With a base score of 100, higher scores indicate a user's credibility within the community. Scores change over time to reflect a user's recent behavior. Users with higher credit ratings enjoy more benefits, while users with lower credit ratings may have some functionality restricted.",
|
||||
"socialCreditsLevelPoor": "Poor",
|
||||
"socialCreditsLevelNormal": "Normal",
|
||||
"socialCreditsLevelGood": "Good",
|
||||
"socialCreditsLevelExcellent": "Excellent",
|
||||
"orderByPopularity": "Sort by popularity",
|
||||
"orderByReleaseDate": "Sort by release date",
|
||||
"editBot": "Edit Bot",
|
||||
"botAutomatedBy": "Automated by {}",
|
||||
"botDetails": "Bot Details",
|
||||
"overview": "Overview",
|
||||
"keys": "Keys",
|
||||
"botNotFound": "Bot not found.",
|
||||
"newBotKey": "New Bot Key",
|
||||
"newBotKeyHint": "Enter a name for your new key. The key will be shown only once.",
|
||||
"revokeBotKey": "Revoke Bot Key",
|
||||
"revokeBotKeyHint": "Are you sure you want to revoke this key? This action cannot be undone and any application using this key will stop working.",
|
||||
"noBotKeys": "No bot keys yet.",
|
||||
"revoke": "Revoke",
|
||||
"keyName": "Key Name",
|
||||
"newKeyGenerated": "New Key Generated",
|
||||
"copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again.",
|
||||
"rotateKey": "Rotate Key",
|
||||
"rotateBotKey": "Rotate Bot Key",
|
||||
"rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone.",
|
||||
"webFeedArticleCount": {
|
||||
"zero": "No articles",
|
||||
"one": "{} article",
|
||||
"other": "{} articles"
|
||||
},
|
||||
"webFeedSubscribed": "The feed has been subscribed",
|
||||
"webFeedUnsubscribed": "The feed has been unsubscribed",
|
||||
"appDetails": "應用程式詳情",
|
||||
"secrets": "密鑰",
|
||||
"appNotFound": "找不到應用程式。",
|
||||
@@ -826,5 +973,107 @@
|
||||
"newSecretGenerated": "已產生新密鑰",
|
||||
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
|
||||
"expiresIn": "過期時間(秒)",
|
||||
"isOidc": "OIDC 相容"
|
||||
}
|
||||
"isOidc": "OIDC 相容",
|
||||
"pinPost": "Pin Post",
|
||||
"unpinPost": "Unpin Post",
|
||||
"pinnedPost": "Pinned",
|
||||
"publisherPage": "Publisher Page",
|
||||
"realmPage": "Realm Page",
|
||||
"replyPage": "Reply Page",
|
||||
"pinPostPublisherHint": "Pin this post to your publisher page",
|
||||
"pinPostRealmHint": "Pin this post to the realm page",
|
||||
"pinPostRealmDisabledHint": "This post doesn't belong to any realm",
|
||||
"pinPostReplyHint": "Pin this post to the reply page",
|
||||
"pinPostReplyDisabledHint": "This post is not a reply",
|
||||
"pin": "Pin",
|
||||
"unpinPostHint": "Are you sure you want to unpin this post?",
|
||||
"all": "All",
|
||||
"statusPresent": "Present",
|
||||
"accountAutomated": "Automated",
|
||||
"chatBreakClearButton": "Clear",
|
||||
"chatBreak5m": "5m",
|
||||
"chatBreak10m": "10m",
|
||||
"chatBreak15m": "15m",
|
||||
"chatBreak30m": "30m",
|
||||
"chatBreakCustomMinutes": "Custom (minutes)",
|
||||
"errorGeneric": "Error: {}",
|
||||
"searchMessages": "Search Messages",
|
||||
"messagesCount": "{} messages",
|
||||
"dotSeparator": "·",
|
||||
"roleValidationHint": "Role must be between 0 and 100",
|
||||
"searchMessagesHint": "Search messages...",
|
||||
"searchLinks": "Links",
|
||||
"searchAttachments": "Attachments",
|
||||
"noMessagesFound": "No messages found",
|
||||
"openInBrowser": "Open in Browser",
|
||||
"highlightPost": "Highlight Post",
|
||||
"filters": "Filters",
|
||||
"apply": "Apply",
|
||||
"pubName": "Pub Name",
|
||||
"realm": "Realm",
|
||||
"shuffle": "Shuffle",
|
||||
"pinned": "Pinned",
|
||||
"noResultsFound": "No results found",
|
||||
"toggleFilters": "Toggle filters",
|
||||
"notableDayNext": "{} is in",
|
||||
"expandPoll": "Expand Poll",
|
||||
"collapsePoll": "Collapse Poll",
|
||||
"embedView": "Embed View",
|
||||
"embedUri": "Embed URI",
|
||||
"aspectRatio": "Aspect Ratio",
|
||||
"renderer": "Renderer",
|
||||
"addEmbed": "Add Embed",
|
||||
"editEmbed": "Edit Embed",
|
||||
"deleteEmbed": "Delete Embed",
|
||||
"deleteEmbedConfirm": "Are you sure you want to delete this embed?",
|
||||
"currentEmbed": "Current Embed",
|
||||
"noEmbed": "No embed yet",
|
||||
"save": "Save",
|
||||
"webView": "Web View",
|
||||
"settingsDefaultPool": "Default file pool",
|
||||
"settingsDefaultPoolHelper": "Select the default storage pool for file uploads",
|
||||
"uploadFile": "Upload File",
|
||||
"authDeviceChallenges": "Device Usage",
|
||||
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
|
||||
"settingsMessageDisplayStyle": "Message Display Style",
|
||||
"auto": "Auto",
|
||||
"manual": "Manual",
|
||||
"iframeCode": "Iframe Code",
|
||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||
"parseIframe": "Parse Iframe",
|
||||
"messageActions": "Message Actions",
|
||||
"viewEmbedLoadHint": "Tap to load",
|
||||
"levelingStage1": "Novice",
|
||||
"levelingStage2": "Apprentice",
|
||||
"levelingStage3": "Journeyman",
|
||||
"levelingStage4": "Adept",
|
||||
"levelingStage5": "Expert",
|
||||
"levelingStage6": "Master",
|
||||
"levelingStage7": "Grandmaster",
|
||||
"levelingStage8": "Legend",
|
||||
"levelingStage9": "Myth",
|
||||
"levelingStage10": "Immortal",
|
||||
"levelingStage11": "Divine",
|
||||
"levelingStage12": "Transcendent",
|
||||
"uploadAttachment": "Upload Attachment",
|
||||
"attachmentPreview": "Attachment Preview",
|
||||
"selectPool": "Select Pool",
|
||||
"choosePool": "Choose a pool",
|
||||
"errorLoadingPools": "Error loading pools",
|
||||
"quotaCostInfo": "This upload will cost {} quota points",
|
||||
"uploadConstraints": "Upload Constraints",
|
||||
"fileSizeExceeded": "File size exceeds the maximum limit of {}",
|
||||
"fileTypeNotAccepted": "File type is not accepted by this pool",
|
||||
"files": "Files",
|
||||
"confirmDeleteFile": "Are you sure you want to delete this file?",
|
||||
"deleteFile": "Delete File",
|
||||
"failedToDeleteFile": "Failed to delete file",
|
||||
"drive": "Drive",
|
||||
"allPools": "All Pools",
|
||||
"includeRecycled": "Include Recycled",
|
||||
"confirmDeleteRecycledFiles": "Are you sure you want to delete all recycled files?",
|
||||
"deleteRecycledFiles": "Delete Recycled Files",
|
||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||
"upload": "Upload"
|
||||
}
|
BIN
assets/images/stickers/angry.png
Normal file
BIN
assets/images/stickers/angry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/stickers/clap.png
Normal file
BIN
assets/images/stickers/clap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/stickers/confuse.png
Normal file
BIN
assets/images/stickers/confuse.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 668 KiB |
BIN
assets/images/stickers/party.png
Normal file
BIN
assets/images/stickers/party.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/stickers/pray.png
Normal file
BIN
assets/images/stickers/pray.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 666 KiB |
BIN
assets/images/stickers/thumb_up.png
Normal file
BIN
assets/images/stickers/thumb_up.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 623 KiB |
@@ -5,3 +5,7 @@ targets:
|
||||
options:
|
||||
explicit_to_json: true
|
||||
field_rename: snake
|
||||
drift_dev:
|
||||
options:
|
||||
databases:
|
||||
app_database: lib/database/drift_db.dart
|
||||
|
1
drift_schemas/app_database/drift_schema_v6.json
Normal file
1
drift_schemas/app_database/drift_schema_v6.json
Normal file
File diff suppressed because one or more lines are too long
@@ -50,18 +50,18 @@ PODS:
|
||||
- Firebase/Messaging (12.2.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.2.0)
|
||||
- firebase_analytics (12.0.1):
|
||||
- firebase_analytics (12.0.2):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.2.0)
|
||||
- Flutter
|
||||
- firebase_core (4.1.0):
|
||||
- firebase_core (4.1.1):
|
||||
- Firebase/CoreOnly (= 12.2.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (5.0.1):
|
||||
- firebase_crashlytics (5.0.2):
|
||||
- Firebase/Crashlytics (= 12.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (16.0.1):
|
||||
- firebase_messaging (16.0.2):
|
||||
- Firebase/Messaging (= 12.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
@@ -149,9 +149,9 @@ PODS:
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (1.1.0):
|
||||
- flutter_webrtc (1.2.0):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 137.7151.03)
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -219,7 +219,7 @@ PODS:
|
||||
- livekit_client (2.5.0):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 137.7151.03)
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -299,7 +299,7 @@ PODS:
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- WebRTC-SDK (137.7151.03)
|
||||
- WebRTC-SDK (137.7151.04)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
@@ -476,10 +476,10 @@ SPEC CHECKSUMS:
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
||||
firebase_analytics: 111ff65791a430356bd6c7e4d7339537fc6a15ae
|
||||
firebase_core: 3ff52146406557dddd01d570e807e203ec7e1302
|
||||
firebase_crashlytics: 3637078b718a52dc9fb4d64e37c969e86b87ff6f
|
||||
firebase_messaging: 3dcc998dd98e1e54af75d0cccae8606eba43553c
|
||||
firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e
|
||||
firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d
|
||||
firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb
|
||||
firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a
|
||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
||||
@@ -499,7 +499,7 @@ SPEC CHECKSUMS:
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_webrtc: b0b2e04411747142962164a1cfa43a1af9a0afac
|
||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
|
||||
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
|
||||
@@ -508,8 +508,8 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
|
||||
livekit_client: f810c81bbbc229a84f60b09e66603ac4e93f7599
|
||||
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
||||
livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
@@ -536,7 +536,7 @@ SPEC CHECKSUMS:
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3
|
||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||
|
||||
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
|
||||
|
||||
|
@@ -566,7 +566,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
|
||||
};
|
||||
4815E0A19398E51078F4160D /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -883,6 +883,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -1096,6 +1097,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -1137,6 +1139,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -1177,6 +1180,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -1434,6 +1438,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -1462,6 +1467,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
@@ -12,7 +12,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 6;
|
||||
int get schemaVersion => 7;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -21,8 +21,8 @@ class AppDatabase extends _$AppDatabase {
|
||||
},
|
||||
onUpgrade: (Migrator m, int from, int to) async {
|
||||
if (from < 2) {
|
||||
// Add isRead column with default value false
|
||||
await m.addColumn(chatMessages, chatMessages.isRead);
|
||||
// Add isDeleted column with default value false
|
||||
await m.addColumn(chatMessages, chatMessages.isDeleted);
|
||||
}
|
||||
if (from < 4) {
|
||||
// Drop old draft tables if they exist
|
||||
@@ -32,6 +32,29 @@ class AppDatabase extends _$AppDatabase {
|
||||
// Migrate from old schema to new schema with separate searchable fields
|
||||
await _migrateToVersion6(m);
|
||||
}
|
||||
if (from < 7) {
|
||||
// Add new columns from SnChatMessage, ignore if they already exist
|
||||
final columnsToAdd = [
|
||||
chatMessages.updatedAt,
|
||||
chatMessages.deletedAt,
|
||||
chatMessages.type,
|
||||
chatMessages.meta,
|
||||
chatMessages.membersMentioned,
|
||||
chatMessages.editedAt,
|
||||
chatMessages.attachments,
|
||||
chatMessages.reactions,
|
||||
chatMessages.repliedMessageId,
|
||||
chatMessages.forwardedMessageId,
|
||||
];
|
||||
|
||||
for (final column in columnsToAdd) {
|
||||
try {
|
||||
await m.addColumn(chatMessages, column);
|
||||
} catch (e) {
|
||||
// Column already exists, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -116,12 +139,6 @@ class AppDatabase extends _$AppDatabase {
|
||||
)).write(ChatMessagesCompanion(status: Value(status)));
|
||||
}
|
||||
|
||||
Future<int> markMessageAsRead(String id) {
|
||||
return (update(chatMessages)..where(
|
||||
(m) => m.id.equals(id),
|
||||
)).write(ChatMessagesCompanion(isRead: const Value(true)));
|
||||
}
|
||||
|
||||
Future<int> deleteMessage(String id) {
|
||||
return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
|
||||
}
|
||||
@@ -134,15 +151,27 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
Future<List<LocalChatMessage>> searchMessages(
|
||||
String roomId,
|
||||
String query,
|
||||
) async {
|
||||
String query, {
|
||||
bool? withAttachments,
|
||||
}) async {
|
||||
var selectStatement = select(chatMessages)
|
||||
..where((m) => m.roomId.equals(roomId));
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
final searchTerm = '%$query%';
|
||||
selectStatement =
|
||||
selectStatement
|
||||
..where((m) => m.content.like('%${query.toLowerCase()}%'));
|
||||
selectStatement..where(
|
||||
(m) =>
|
||||
m.content.like(searchTerm) |
|
||||
m.meta.like(searchTerm) |
|
||||
m.attachments.like(searchTerm) |
|
||||
m.type.like(searchTerm),
|
||||
);
|
||||
}
|
||||
|
||||
if (withAttachments == true) {
|
||||
selectStatement =
|
||||
selectStatement..where((m) => m.attachments.equals('[]').not());
|
||||
}
|
||||
|
||||
final messages =
|
||||
@@ -154,16 +183,26 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
// Convert between Drift and model objects
|
||||
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
|
||||
final remote = message.toRemoteMessage();
|
||||
return ChatMessagesCompanion(
|
||||
id: Value(message.id),
|
||||
roomId: Value(message.roomId),
|
||||
senderId: Value(message.senderId),
|
||||
content: Value(message.toRemoteMessage().content),
|
||||
content: Value(remote.content),
|
||||
nonce: Value(message.nonce),
|
||||
data: Value(jsonEncode(message.data)),
|
||||
createdAt: Value(message.createdAt),
|
||||
status: Value(message.status),
|
||||
isRead: Value(message.isRead),
|
||||
updatedAt: Value(remote.updatedAt),
|
||||
deletedAt: Value(remote.deletedAt),
|
||||
type: Value(remote.type),
|
||||
meta: Value(remote.meta),
|
||||
membersMentioned: Value(remote.membersMentioned),
|
||||
editedAt: Value(remote.editedAt),
|
||||
attachments: Value(remote.attachments.map((e) => e.toJson()).toList()),
|
||||
reactions: Value(remote.reactions.map((e) => e.toJson()).toList()),
|
||||
repliedMessageId: Value(remote.repliedMessageId),
|
||||
forwardedMessageId: Value(remote.forwardedMessageId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,7 +216,18 @@ class AppDatabase extends _$AppDatabase {
|
||||
createdAt: dbMessage.createdAt,
|
||||
status: dbMessage.status,
|
||||
nonce: dbMessage.nonce,
|
||||
isRead: dbMessage.isRead,
|
||||
content: dbMessage.content,
|
||||
isDeleted: dbMessage.isDeleted,
|
||||
updatedAt: dbMessage.updatedAt,
|
||||
deletedAt: dbMessage.deletedAt,
|
||||
type: dbMessage.type,
|
||||
meta: dbMessage.meta,
|
||||
membersMentioned: dbMessage.membersMentioned,
|
||||
editedAt: dbMessage.editedAt,
|
||||
attachments: dbMessage.attachments,
|
||||
reactions: dbMessage.reactions,
|
||||
repliedMessageId: dbMessage.repliedMessageId,
|
||||
forwardedMessageId: dbMessage.forwardedMessageId,
|
||||
);
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,41 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
|
||||
class MapConverter extends TypeConverter<Map<String, dynamic>, String> {
|
||||
const MapConverter();
|
||||
|
||||
@override
|
||||
Map<String, dynamic> fromSql(String fromDb) => json.decode(fromDb);
|
||||
|
||||
@override
|
||||
String toSql(Map<String, dynamic> value) => json.encode(value);
|
||||
}
|
||||
|
||||
class ListStringConverter extends TypeConverter<List<String>, String> {
|
||||
const ListStringConverter();
|
||||
|
||||
@override
|
||||
List<String> fromSql(String fromDb) => List<String>.from(json.decode(fromDb));
|
||||
|
||||
@override
|
||||
String toSql(List<String> value) => json.encode(value);
|
||||
}
|
||||
|
||||
class ListMapConverter
|
||||
extends TypeConverter<List<Map<String, dynamic>>, String> {
|
||||
const ListMapConverter();
|
||||
|
||||
@override
|
||||
List<Map<String, dynamic>> fromSql(String fromDb) =>
|
||||
List<Map<String, dynamic>>.from(json.decode(fromDb));
|
||||
|
||||
@override
|
||||
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
||||
}
|
||||
|
||||
class ChatMessages extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get roomId => text()();
|
||||
@@ -11,7 +45,24 @@ class ChatMessages extends Table {
|
||||
TextColumn get data => text()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
IntColumn get status => intEnum<MessageStatus>()();
|
||||
BoolColumn get isRead => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isDeleted =>
|
||||
boolean().nullable().withDefault(const Constant(false))();
|
||||
DateTimeColumn get updatedAt => dateTime().nullable()();
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
TextColumn get type => text().withDefault(const Constant('text'))();
|
||||
TextColumn get meta =>
|
||||
text().map(const MapConverter()).withDefault(const Constant('{}'))();
|
||||
TextColumn get membersMentioned =>
|
||||
text()
|
||||
.map(const ListStringConverter())
|
||||
.withDefault(const Constant('[]'))();
|
||||
DateTimeColumn get editedAt => dateTime().nullable()();
|
||||
TextColumn get attachments =>
|
||||
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
|
||||
TextColumn get reactions =>
|
||||
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
|
||||
TextColumn get repliedMessageId => text().nullable()();
|
||||
TextColumn get forwardedMessageId => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
@@ -25,8 +76,19 @@ class LocalChatMessage {
|
||||
final DateTime createdAt;
|
||||
MessageStatus status;
|
||||
final String? nonce;
|
||||
final String? content;
|
||||
final bool? isDeleted;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final String type;
|
||||
final Map<String, dynamic> meta;
|
||||
final List<String> membersMentioned;
|
||||
final DateTime? editedAt;
|
||||
final List<Map<String, dynamic>> attachments;
|
||||
final List<Map<String, dynamic>> reactions;
|
||||
final String? repliedMessageId;
|
||||
final String? forwardedMessageId;
|
||||
List<UniversalFile>? localAttachments;
|
||||
bool isRead;
|
||||
|
||||
LocalChatMessage({
|
||||
required this.id,
|
||||
@@ -36,8 +98,19 @@ class LocalChatMessage {
|
||||
required this.createdAt,
|
||||
required this.nonce,
|
||||
required this.status,
|
||||
this.content,
|
||||
this.isDeleted,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.type,
|
||||
required this.meta,
|
||||
required this.membersMentioned,
|
||||
this.editedAt,
|
||||
required this.attachments,
|
||||
required this.reactions,
|
||||
this.repliedMessageId,
|
||||
this.forwardedMessageId,
|
||||
this.localAttachments,
|
||||
this.isRead = false,
|
||||
});
|
||||
|
||||
SnChatMessage toRemoteMessage() {
|
||||
@@ -48,7 +121,6 @@ class LocalChatMessage {
|
||||
SnChatMessage message,
|
||||
MessageStatus status, {
|
||||
String? nonce,
|
||||
bool isRead = false,
|
||||
}) {
|
||||
return LocalChatMessage(
|
||||
id: message.id,
|
||||
@@ -58,7 +130,18 @@ class LocalChatMessage {
|
||||
createdAt: message.createdAt,
|
||||
status: status,
|
||||
nonce: nonce ?? message.nonce,
|
||||
isRead: isRead,
|
||||
content: message.content,
|
||||
isDeleted: false,
|
||||
updatedAt: message.updatedAt,
|
||||
deletedAt: null,
|
||||
type: message.type,
|
||||
meta: message.meta,
|
||||
membersMentioned: message.membersMentioned,
|
||||
editedAt: message.editedAt,
|
||||
attachments: message.attachments.map((e) => e.toJson()).toList(),
|
||||
reactions: message.reactions.map((e) => e.toJson()).toList(),
|
||||
repliedMessageId: message.repliedMessageId,
|
||||
forwardedMessageId: message.forwardedMessageId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -17,7 +16,7 @@ import 'package:island/firebase_options.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/theme.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.dart';
|
||||
@@ -30,6 +29,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
@@ -89,31 +89,42 @@ void main() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
||||
doWhenWindowReady(() {
|
||||
const defaultSize = Size(360, 640);
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
// Get saved window size from preferences
|
||||
final savedSizeString = prefs.getString(kAppWindowSize);
|
||||
Size initialSize = defaultSize;
|
||||
const defaultSize = Size(360, 640);
|
||||
|
||||
if (savedSizeString != null) {
|
||||
try {
|
||||
final parts = savedSizeString.split(',');
|
||||
if (parts.length == 2) {
|
||||
final width = double.parse(parts[0]);
|
||||
final height = double.parse(parts[1]);
|
||||
initialSize = Size(width, height);
|
||||
}
|
||||
} catch (e) {
|
||||
log("[SplashScreen] Failed to parse saved window size: $e");
|
||||
initialSize = defaultSize;
|
||||
// Get saved window size from preferences
|
||||
final savedSizeString = prefs.getString(kAppWindowSize);
|
||||
Size initialSize = defaultSize;
|
||||
|
||||
if (savedSizeString != null) {
|
||||
try {
|
||||
final parts = savedSizeString.split(',');
|
||||
if (parts.length == 2) {
|
||||
final width = double.parse(parts[0]);
|
||||
final height = double.parse(parts[1]);
|
||||
initialSize = Size(width, height);
|
||||
}
|
||||
} catch (e) {
|
||||
log("[SplashScreen] Failed to parse saved window size: $e");
|
||||
initialSize = defaultSize;
|
||||
}
|
||||
}
|
||||
|
||||
appWindow.minSize = defaultSize;
|
||||
appWindow.size = initialSize;
|
||||
appWindow.alignment = Alignment.center;
|
||||
appWindow.show();
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: initialSize,
|
||||
center: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
windowButtonVisibility: true,
|
||||
);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.setMinimumSize(defaultSize);
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
final opacity = prefs.getDouble(kAppWindowOpacity) ?? 1.0;
|
||||
await windowManager.setOpacity(opacity);
|
||||
log(
|
||||
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}",
|
||||
);
|
||||
@@ -181,7 +192,7 @@ class IslandApp extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -235,6 +246,7 @@ class IslandApp extends HookConsumerWidget {
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
color: Colors.transparent,
|
||||
theme: theme?.light,
|
||||
darkTheme: theme?.dark,
|
||||
themeMode: ThemeMode.system,
|
||||
|
@@ -98,6 +98,7 @@ sealed class SnAccountStatus with _$SnAccountStatus {
|
||||
required bool isNotDisturb,
|
||||
required bool isCustomized,
|
||||
@Default("") String label,
|
||||
required Map<String, dynamic>? meta,
|
||||
required DateTime? clearedAt,
|
||||
required String accountId,
|
||||
required DateTime createdAt,
|
||||
|
@@ -1053,7 +1053,7 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
|
||||
/// @nodoc
|
||||
mixin _$SnAccountStatus {
|
||||
|
||||
String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; Map<String, dynamic>? get meta; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1066,16 +1066,16 @@ $SnAccountStatusCopyWith<SnAccountStatus> get copyWith => _$SnAccountStatusCopyW
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(meta),clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -1086,7 +1086,7 @@ abstract mixin class $SnAccountStatusCopyWith<$Res> {
|
||||
factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -1103,7 +1103,7 @@ class _$SnAccountStatusCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1112,7 +1112,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig
|
||||
as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1199,10 +1200,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountStatus() when $default != null:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -1220,10 +1221,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountStatus():
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -1237,10 +1238,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountStatus() when $default != null:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -1252,7 +1253,7 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnAccountStatus implements SnAccountStatus {
|
||||
const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
||||
const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required final Map<String, dynamic>? meta, required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta;
|
||||
factory _SnAccountStatus.fromJson(Map<String, dynamic> json) => _$SnAccountStatusFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@@ -1262,6 +1263,15 @@ class _SnAccountStatus implements SnAccountStatus {
|
||||
@override final bool isNotDisturb;
|
||||
@override final bool isCustomized;
|
||||
@override@JsonKey() final String label;
|
||||
final Map<String, dynamic>? _meta;
|
||||
@override Map<String, dynamic>? get meta {
|
||||
final value = _meta;
|
||||
if (value == null) return null;
|
||||
if (_meta is EqualUnmodifiableMapView) return _meta;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final DateTime? clearedAt;
|
||||
@override final String accountId;
|
||||
@override final DateTime createdAt;
|
||||
@@ -1281,16 +1291,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(_meta),clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -1301,7 +1311,7 @@ abstract mixin class _$SnAccountStatusCopyWith<$Res> implements $SnAccountStatus
|
||||
factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -1318,7 +1328,7 @@ class __$SnAccountStatusCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAccountStatus(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1327,7 +1337,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig
|
||||
as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -158,6 +158,7 @@ _SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) =>
|
||||
isNotDisturb: json['is_not_disturb'] as bool,
|
||||
isCustomized: json['is_customized'] as bool,
|
||||
label: json['label'] as String? ?? "",
|
||||
meta: json['meta'] as Map<String, dynamic>?,
|
||||
clearedAt:
|
||||
json['cleared_at'] == null
|
||||
? null
|
||||
@@ -180,6 +181,7 @@ Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
|
||||
'is_not_disturb': instance.isNotDisturb,
|
||||
'is_customized': instance.isCustomized,
|
||||
'label': instance.label,
|
||||
'meta': instance.meta,
|
||||
'cleared_at': instance.clearedAt?.toIso8601String(),
|
||||
'account_id': instance.accountId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@@ -14,11 +14,11 @@ sealed class AppToken with _$AppToken {
|
||||
@freezed
|
||||
sealed class GeoIpLocation with _$GeoIpLocation {
|
||||
const factory GeoIpLocation({
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
required String countryCode,
|
||||
required String country,
|
||||
required String city,
|
||||
required double? latitude,
|
||||
required double? longitude,
|
||||
required String? countryCode,
|
||||
required String? country,
|
||||
required String? city,
|
||||
}) = _GeoIpLocation;
|
||||
|
||||
factory GeoIpLocation.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -29,7 +29,7 @@ sealed class GeoIpLocation with _$GeoIpLocation {
|
||||
sealed class SnAuthChallenge with _$SnAuthChallenge {
|
||||
const factory SnAuthChallenge({
|
||||
required String id,
|
||||
required DateTime expiredAt,
|
||||
required DateTime? expiredAt,
|
||||
required int stepRemain,
|
||||
required int stepTotal,
|
||||
required int failedAttempts,
|
||||
@@ -57,7 +57,7 @@ sealed class SnAuthSession with _$SnAuthSession {
|
||||
required String id,
|
||||
required String? label,
|
||||
required DateTime lastGrantedAt,
|
||||
required DateTime expiredAt,
|
||||
required DateTime? expiredAt,
|
||||
required String accountId,
|
||||
required String challengeId,
|
||||
required SnAuthChallenge challenge,
|
||||
|
@@ -272,7 +272,7 @@ as String,
|
||||
/// @nodoc
|
||||
mixin _$GeoIpLocation {
|
||||
|
||||
double get latitude; double get longitude; String get countryCode; String get country; String get city;
|
||||
double? get latitude; double? get longitude; String? get countryCode; String? get country; String? get city;
|
||||
/// Create a copy of GeoIpLocation
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -305,7 +305,7 @@ abstract mixin class $GeoIpLocationCopyWith<$Res> {
|
||||
factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
double latitude, double longitude, String countryCode, String country, String city
|
||||
double? latitude, double? longitude, String? countryCode, String? country, String? city
|
||||
});
|
||||
|
||||
|
||||
@@ -322,14 +322,14 @@ class _$GeoIpLocationCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of GeoIpLocation
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
|
||||
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
|
||||
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
|
||||
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
|
||||
as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
|
||||
as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double latitude, double longitude, String countryCode, String country, String city)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double? latitude, double? longitude, String? countryCode, String? country, String? city)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _GeoIpLocation() when $default != null:
|
||||
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
|
||||
@@ -432,7 +432,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double latitude, double longitude, String countryCode, String country, String city) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double? latitude, double? longitude, String? countryCode, String? country, String? city) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _GeoIpLocation():
|
||||
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);}
|
||||
@@ -449,7 +449,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double latitude, double longitude, String countryCode, String country, String city)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double? latitude, double? longitude, String? countryCode, String? country, String? city)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _GeoIpLocation() when $default != null:
|
||||
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
|
||||
@@ -467,11 +467,11 @@ class _GeoIpLocation implements GeoIpLocation {
|
||||
const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city});
|
||||
factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json);
|
||||
|
||||
@override final double latitude;
|
||||
@override final double longitude;
|
||||
@override final String countryCode;
|
||||
@override final String country;
|
||||
@override final String city;
|
||||
@override final double? latitude;
|
||||
@override final double? longitude;
|
||||
@override final String? countryCode;
|
||||
@override final String? country;
|
||||
@override final String? city;
|
||||
|
||||
/// Create a copy of GeoIpLocation
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -506,7 +506,7 @@ abstract mixin class _$GeoIpLocationCopyWith<$Res> implements $GeoIpLocationCopy
|
||||
factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
double latitude, double longitude, String countryCode, String country, String city
|
||||
double? latitude, double? longitude, String? countryCode, String? country, String? city
|
||||
});
|
||||
|
||||
|
||||
@@ -523,14 +523,14 @@ class __$GeoIpLocationCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of GeoIpLocation
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
|
||||
return _then(_GeoIpLocation(
|
||||
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
|
||||
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
|
||||
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
|
||||
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
|
||||
as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
|
||||
as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@ as String,
|
||||
/// @nodoc
|
||||
mixin _$SnAuthChallenge {
|
||||
|
||||
String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; DateTime? get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAuthChallenge
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -574,7 +574,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res> {
|
||||
factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -591,11 +591,11 @@ class _$SnAuthChallengeCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthChallenge
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
|
||||
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
@@ -704,7 +704,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthChallenge() when $default != null:
|
||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -725,7 +725,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthChallenge():
|
||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
@@ -742,7 +742,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthChallenge() when $default != null:
|
||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -761,7 +761,7 @@ class _SnAuthChallenge implements SnAuthChallenge {
|
||||
factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final DateTime expiredAt;
|
||||
@override final DateTime? expiredAt;
|
||||
@override final int stepRemain;
|
||||
@override final int stepTotal;
|
||||
@override final int failedAttempts;
|
||||
@@ -829,7 +829,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
|
||||
factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -846,11 +846,11 @@ class __$SnAuthChallengeCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthChallenge
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAuthChallenge(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
|
||||
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
@@ -888,7 +888,7 @@ $GeoIpLocationCopyWith<$Res>? get location {
|
||||
/// @nodoc
|
||||
mixin _$SnAuthSession {
|
||||
|
||||
String get id; String? get label; DateTime get lastGrantedAt; DateTime get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; String? get label; DateTime get lastGrantedAt; DateTime? get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAuthSession
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -921,7 +921,7 @@ abstract mixin class $SnAuthSessionCopyWith<$Res> {
|
||||
factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -938,13 +938,13 @@ class _$SnAuthSessionCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthSession
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
|
||||
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1041,7 +1041,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthSession() when $default != null:
|
||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -1062,7 +1062,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthSession():
|
||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
@@ -1079,7 +1079,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthSession() when $default != null:
|
||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -1100,7 +1100,7 @@ class _SnAuthSession implements SnAuthSession {
|
||||
@override final String id;
|
||||
@override final String? label;
|
||||
@override final DateTime lastGrantedAt;
|
||||
@override final DateTime expiredAt;
|
||||
@override final DateTime? expiredAt;
|
||||
@override final String accountId;
|
||||
@override final String challengeId;
|
||||
@override final SnAuthChallenge challenge;
|
||||
@@ -1141,7 +1141,7 @@ abstract mixin class _$SnAuthSessionCopyWith<$Res> implements $SnAuthSessionCopy
|
||||
factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -1158,13 +1158,13 @@ class __$SnAuthSessionCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthSession
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAuthSession(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
|
||||
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -15,11 +15,11 @@ Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{
|
||||
|
||||
_GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) =>
|
||||
_GeoIpLocation(
|
||||
latitude: (json['latitude'] as num).toDouble(),
|
||||
longitude: (json['longitude'] as num).toDouble(),
|
||||
countryCode: json['country_code'] as String,
|
||||
country: json['country'] as String,
|
||||
city: json['city'] as String,
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
countryCode: json['country_code'] as String?,
|
||||
country: json['country'] as String?,
|
||||
city: json['city'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
|
||||
@@ -34,7 +34,10 @@ Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
|
||||
_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
|
||||
_SnAuthChallenge(
|
||||
id: json['id'] as String,
|
||||
expiredAt: DateTime.parse(json['expired_at'] as String),
|
||||
expiredAt:
|
||||
json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
stepRemain: (json['step_remain'] as num).toInt(),
|
||||
stepTotal: (json['step_total'] as num).toInt(),
|
||||
failedAttempts: (json['failed_attempts'] as num).toInt(),
|
||||
@@ -66,7 +69,7 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'expired_at': instance.expiredAt.toIso8601String(),
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'step_remain': instance.stepRemain,
|
||||
'step_total': instance.stepTotal,
|
||||
'failed_attempts': instance.failedAttempts,
|
||||
@@ -89,7 +92,10 @@ _SnAuthSession _$SnAuthSessionFromJson(Map<String, dynamic> json) =>
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String?,
|
||||
lastGrantedAt: DateTime.parse(json['last_granted_at'] as String),
|
||||
expiredAt: DateTime.parse(json['expired_at'] as String),
|
||||
expiredAt:
|
||||
json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
accountId: json['account_id'] as String,
|
||||
challengeId: json['challenge_id'] as String,
|
||||
challenge: SnAuthChallenge.fromJson(
|
||||
@@ -108,7 +114,7 @@ Map<String, dynamic> _$SnAuthSessionToJson(_SnAuthSession instance) =>
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
'last_granted_at': instance.lastGrantedAt.toIso8601String(),
|
||||
'expired_at': instance.expiredAt.toIso8601String(),
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'account_id': instance.accountId,
|
||||
'challenge_id': instance.challengeId,
|
||||
'challenge': instance.challenge.toJson(),
|
||||
|
@@ -40,7 +40,7 @@ sealed class SnChatMessage with _$SnChatMessage {
|
||||
String? content,
|
||||
String? nonce,
|
||||
@Default({}) Map<String, dynamic> meta,
|
||||
@Default([]) List<String> membersMetioned,
|
||||
@Default([]) List<String> membersMentioned,
|
||||
DateTime? editedAt,
|
||||
@Default([]) List<SnCloudFile> attachments,
|
||||
@Default([]) List<SnChatReaction> reactions,
|
||||
@@ -117,23 +117,10 @@ class MessageChangeAction {
|
||||
static const String delete = "delete";
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class MessageChange with _$MessageChange {
|
||||
const factory MessageChange({
|
||||
required String messageId,
|
||||
required String action,
|
||||
SnChatMessage? message,
|
||||
required DateTime timestamp,
|
||||
}) = _MessageChange;
|
||||
|
||||
factory MessageChange.fromJson(Map<String, dynamic> json) =>
|
||||
_$MessageChangeFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class MessageSyncResponse with _$MessageSyncResponse {
|
||||
const factory MessageSyncResponse({
|
||||
@Default([]) List<MessageChange> changes,
|
||||
@Default([]) List<SnChatMessage> messages,
|
||||
required DateTime currentTimestamp,
|
||||
}) = _MessageSyncResponse;
|
||||
|
||||
|
@@ -391,7 +391,7 @@ $SnRealmCopyWith<$Res>? get realm {
|
||||
/// @nodoc
|
||||
mixin _$SnChatMessage {
|
||||
|
||||
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId;
|
||||
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMentioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId;
|
||||
/// Create a copy of SnChatMessage
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -404,16 +404,16 @@ $SnChatMessageCopyWith<SnChatMessage> get copyWith => _$SnChatMessageCopyWithImp
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMentioned, membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMentioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
}
|
||||
|
||||
|
||||
@@ -424,7 +424,7 @@ abstract mixin class $SnChatMessageCopyWith<$Res> {
|
||||
factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
});
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ class _$SnChatMessageCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnChatMessage
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
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
|
||||
@@ -451,7 +451,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non
|
||||
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
|
||||
as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self.membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self.membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnCloudFile>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable
|
||||
@@ -551,10 +551,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnChatMessage() when $default != null:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -572,10 +572,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnChatMessage():
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);}
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -589,10 +589,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnChatMessage() when $default != null:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -604,7 +604,7 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnChatMessage implements SnChatMessage {
|
||||
const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMetioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions;
|
||||
const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMentioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMentioned = membersMentioned,_attachments = attachments,_reactions = reactions;
|
||||
factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json);
|
||||
|
||||
@override final DateTime createdAt;
|
||||
@@ -621,11 +621,11 @@ class _SnChatMessage implements SnChatMessage {
|
||||
return EqualUnmodifiableMapView(_meta);
|
||||
}
|
||||
|
||||
final List<String> _membersMetioned;
|
||||
@override@JsonKey() List<String> get membersMetioned {
|
||||
if (_membersMetioned is EqualUnmodifiableListView) return _membersMetioned;
|
||||
final List<String> _membersMentioned;
|
||||
@override@JsonKey() List<String> get membersMentioned {
|
||||
if (_membersMentioned is EqualUnmodifiableListView) return _membersMentioned;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_membersMetioned);
|
||||
return EqualUnmodifiableListView(_membersMentioned);
|
||||
}
|
||||
|
||||
@override final DateTime? editedAt;
|
||||
@@ -662,16 +662,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMentioned, _membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMentioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
}
|
||||
|
||||
|
||||
@@ -682,7 +682,7 @@ abstract mixin class _$SnChatMessageCopyWith<$Res> implements $SnChatMessageCopy
|
||||
factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
});
|
||||
|
||||
|
||||
@@ -699,7 +699,7 @@ class __$SnChatMessageCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnChatMessage
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
return _then(_SnChatMessage(
|
||||
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
|
||||
@@ -709,7 +709,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non
|
||||
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
|
||||
as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self._membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self._membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnCloudFile>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1691,300 +1691,10 @@ $SnChatMessageCopyWith<$Res>? get lastMessage {
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MessageChange {
|
||||
|
||||
String get messageId; String get action; SnChatMessage? get message; DateTime get timestamp;
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$MessageChangeCopyWith<MessageChange> get copyWith => _$MessageChangeCopyWithImpl<MessageChange>(this as MessageChange, _$identity);
|
||||
|
||||
/// Serializes this MessageChange to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $MessageChangeCopyWith<$Res> {
|
||||
factory $MessageChangeCopyWith(MessageChange value, $Res Function(MessageChange) _then) = _$MessageChangeCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String messageId, String action, SnChatMessage? message, DateTime timestamp
|
||||
});
|
||||
|
||||
|
||||
$SnChatMessageCopyWith<$Res>? get message;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$MessageChangeCopyWithImpl<$Res>
|
||||
implements $MessageChangeCopyWith<$Res> {
|
||||
_$MessageChangeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final MessageChange _self;
|
||||
final $Res Function(MessageChange) _then;
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable
|
||||
as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
|
||||
as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnChatMessageCopyWith<$Res>? get message {
|
||||
if (_self.message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnChatMessageCopyWith<$Res>(_self.message!, (value) {
|
||||
return _then(_self.copyWith(message: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [MessageChange].
|
||||
extension MessageChangePatterns on MessageChange {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _MessageChange value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _MessageChange value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _MessageChange value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String messageId, String action, SnChatMessage? message, DateTime timestamp)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String messageId, String action, SnChatMessage? message, DateTime timestamp) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange():
|
||||
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String messageId, String action, SnChatMessage? message, DateTime timestamp)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _MessageChange implements MessageChange {
|
||||
const _MessageChange({required this.messageId, required this.action, this.message, required this.timestamp});
|
||||
factory _MessageChange.fromJson(Map<String, dynamic> json) => _$MessageChangeFromJson(json);
|
||||
|
||||
@override final String messageId;
|
||||
@override final String action;
|
||||
@override final SnChatMessage? message;
|
||||
@override final DateTime timestamp;
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$MessageChangeCopyWith<_MessageChange> get copyWith => __$MessageChangeCopyWithImpl<_MessageChange>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$MessageChangeToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$MessageChangeCopyWith<$Res> implements $MessageChangeCopyWith<$Res> {
|
||||
factory _$MessageChangeCopyWith(_MessageChange value, $Res Function(_MessageChange) _then) = __$MessageChangeCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String messageId, String action, SnChatMessage? message, DateTime timestamp
|
||||
});
|
||||
|
||||
|
||||
@override $SnChatMessageCopyWith<$Res>? get message;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$MessageChangeCopyWithImpl<$Res>
|
||||
implements _$MessageChangeCopyWith<$Res> {
|
||||
__$MessageChangeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _MessageChange _self;
|
||||
final $Res Function(_MessageChange) _then;
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) {
|
||||
return _then(_MessageChange(
|
||||
messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable
|
||||
as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
|
||||
as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnChatMessageCopyWith<$Res>? get message {
|
||||
if (_self.message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnChatMessageCopyWith<$Res>(_self.message!, (value) {
|
||||
return _then(_self.copyWith(message: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MessageSyncResponse {
|
||||
|
||||
List<MessageChange> get changes; DateTime get currentTimestamp;
|
||||
List<SnChatMessage> get messages; DateTime get currentTimestamp;
|
||||
/// Create a copy of MessageSyncResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1997,16 +1707,16 @@ $MessageSyncResponseCopyWith<MessageSyncResponse> get copyWith => _$MessageSyncR
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.changes, changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.messages, messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(changes),currentTimestamp);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(messages),currentTimestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)';
|
||||
return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)';
|
||||
}
|
||||
|
||||
|
||||
@@ -2017,7 +1727,7 @@ abstract mixin class $MessageSyncResponseCopyWith<$Res> {
|
||||
factory $MessageSyncResponseCopyWith(MessageSyncResponse value, $Res Function(MessageSyncResponse) _then) = _$MessageSyncResponseCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<MessageChange> changes, DateTime currentTimestamp
|
||||
List<SnChatMessage> messages, DateTime currentTimestamp
|
||||
});
|
||||
|
||||
|
||||
@@ -2034,10 +1744,10 @@ class _$MessageSyncResponseCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of MessageSyncResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? changes = null,Object? currentTimestamp = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? messages = null,Object? currentTimestamp = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
changes: null == changes ? _self.changes : changes // ignore: cast_nullable_to_non_nullable
|
||||
as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
messages: null == messages ? _self.messages : messages // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
@@ -2120,10 +1830,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<MessageChange> changes, DateTime currentTimestamp)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<SnChatMessage> messages, DateTime currentTimestamp)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageSyncResponse() when $default != null:
|
||||
return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
return $default(_that.messages,_that.currentTimestamp);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -2141,10 +1851,10 @@ return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<MessageChange> changes, DateTime currentTimestamp) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<SnChatMessage> messages, DateTime currentTimestamp) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageSyncResponse():
|
||||
return $default(_that.changes,_that.currentTimestamp);}
|
||||
return $default(_that.messages,_that.currentTimestamp);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -2158,10 +1868,10 @@ return $default(_that.changes,_that.currentTimestamp);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<MessageChange> changes, DateTime currentTimestamp)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<SnChatMessage> messages, DateTime currentTimestamp)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageSyncResponse() when $default != null:
|
||||
return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
return $default(_that.messages,_that.currentTimestamp);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -2173,14 +1883,14 @@ return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _MessageSyncResponse implements MessageSyncResponse {
|
||||
const _MessageSyncResponse({final List<MessageChange> changes = const [], required this.currentTimestamp}): _changes = changes;
|
||||
const _MessageSyncResponse({final List<SnChatMessage> messages = const [], required this.currentTimestamp}): _messages = messages;
|
||||
factory _MessageSyncResponse.fromJson(Map<String, dynamic> json) => _$MessageSyncResponseFromJson(json);
|
||||
|
||||
final List<MessageChange> _changes;
|
||||
@override@JsonKey() List<MessageChange> get changes {
|
||||
if (_changes is EqualUnmodifiableListView) return _changes;
|
||||
final List<SnChatMessage> _messages;
|
||||
@override@JsonKey() List<SnChatMessage> get messages {
|
||||
if (_messages is EqualUnmodifiableListView) return _messages;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_changes);
|
||||
return EqualUnmodifiableListView(_messages);
|
||||
}
|
||||
|
||||
@override final DateTime currentTimestamp;
|
||||
@@ -2198,16 +1908,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._changes, _changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._messages, _messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_changes),currentTimestamp);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_messages),currentTimestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)';
|
||||
return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)';
|
||||
}
|
||||
|
||||
|
||||
@@ -2218,7 +1928,7 @@ abstract mixin class _$MessageSyncResponseCopyWith<$Res> implements $MessageSync
|
||||
factory _$MessageSyncResponseCopyWith(_MessageSyncResponse value, $Res Function(_MessageSyncResponse) _then) = __$MessageSyncResponseCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<MessageChange> changes, DateTime currentTimestamp
|
||||
List<SnChatMessage> messages, DateTime currentTimestamp
|
||||
});
|
||||
|
||||
|
||||
@@ -2235,10 +1945,10 @@ class __$MessageSyncResponseCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of MessageSyncResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? changes = null,Object? currentTimestamp = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? messages = null,Object? currentTimestamp = null,}) {
|
||||
return _then(_MessageSyncResponse(
|
||||
changes: null == changes ? _self._changes : changes // ignore: cast_nullable_to_non_nullable
|
||||
as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
messages: null == messages ? _self._messages : messages // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
@@ -69,8 +69,8 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>
|
||||
content: json['content'] as String?,
|
||||
nonce: json['nonce'] as String?,
|
||||
meta: json['meta'] as Map<String, dynamic>? ?? const {},
|
||||
membersMetioned:
|
||||
(json['members_metioned'] as List<dynamic>?)
|
||||
membersMentioned:
|
||||
(json['members_mentioned'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
@@ -105,7 +105,7 @@ Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) =>
|
||||
'content': instance.content,
|
||||
'nonce': instance.nonce,
|
||||
'meta': instance.meta,
|
||||
'members_metioned': instance.membersMetioned,
|
||||
'members_mentioned': instance.membersMentioned,
|
||||
'edited_at': instance.editedAt?.toIso8601String(),
|
||||
'attachments': instance.attachments.map((e) => e.toJson()).toList(),
|
||||
'reactions': instance.reactions.map((e) => e.toJson()).toList(),
|
||||
@@ -227,30 +227,11 @@ Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
|
||||
'last_message': instance.lastMessage?.toJson(),
|
||||
};
|
||||
|
||||
_MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) =>
|
||||
_MessageChange(
|
||||
messageId: json['message_id'] as String,
|
||||
action: json['action'] as String,
|
||||
message:
|
||||
json['message'] == null
|
||||
? null
|
||||
: SnChatMessage.fromJson(json['message'] as Map<String, dynamic>),
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MessageChangeToJson(_MessageChange instance) =>
|
||||
<String, dynamic>{
|
||||
'message_id': instance.messageId,
|
||||
'action': instance.action,
|
||||
'message': instance.message?.toJson(),
|
||||
'timestamp': instance.timestamp.toIso8601String(),
|
||||
};
|
||||
|
||||
_MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) =>
|
||||
_MessageSyncResponse(
|
||||
changes:
|
||||
(json['changes'] as List<dynamic>?)
|
||||
?.map((e) => MessageChange.fromJson(e as Map<String, dynamic>))
|
||||
messages:
|
||||
(json['messages'] as List<dynamic>?)
|
||||
?.map((e) => SnChatMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
currentTimestamp: DateTime.parse(json['current_timestamp'] as String),
|
||||
@@ -259,7 +240,7 @@ _MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$MessageSyncResponseToJson(
|
||||
_MessageSyncResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'changes': instance.changes.map((e) => e.toJson()).toList(),
|
||||
'messages': instance.messages.map((e) => e.toJson()).toList(),
|
||||
'current_timestamp': instance.currentTimestamp.toIso8601String(),
|
||||
};
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
|
||||
part 'file.freezed.dart';
|
||||
part 'file.g.dart';
|
||||
@@ -42,6 +43,7 @@ sealed class SnCloudFile with _$SnCloudFile {
|
||||
required String? description,
|
||||
required Map<String, dynamic>? fileMeta,
|
||||
required Map<String, dynamic>? userMeta,
|
||||
required SnFilePool? pool,
|
||||
@Default([]) List<int> sensitiveMarks,
|
||||
required String? mimeType,
|
||||
required String? hash,
|
||||
|
@@ -278,7 +278,7 @@ as bool,
|
||||
/// @nodoc
|
||||
mixin _$SnCloudFile {
|
||||
|
||||
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; SnFilePool? get pool; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -291,16 +291,16 @@ $SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCl
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),pool,const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -311,11 +311,11 @@ abstract mixin class $SnCloudFileCopyWith<$Res> {
|
||||
factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
$SnFilePoolCopyWith<$Res>? get pool;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -328,14 +328,15 @@ class _$SnCloudFileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
|
||||
as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
|
||||
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||
@@ -347,7 +348,19 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFilePoolCopyWith<$Res>? get pool {
|
||||
if (_self.pool == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) {
|
||||
return _then(_self.copyWith(pool: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -426,10 +439,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFile() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -447,10 +460,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFile():
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -464,10 +477,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFile() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -479,7 +492,7 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnCloudFile implements SnCloudFile {
|
||||
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, final List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks;
|
||||
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, required this.pool, final List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks;
|
||||
factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@@ -503,6 +516,7 @@ class _SnCloudFile implements SnCloudFile {
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final SnFilePool? pool;
|
||||
final List<int> _sensitiveMarks;
|
||||
@override@JsonKey() List<int> get sensitiveMarks {
|
||||
if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks;
|
||||
@@ -532,16 +546,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),pool,const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -552,11 +566,11 @@ abstract mixin class _$SnCloudFileCopyWith<$Res> implements $SnCloudFileCopyWith
|
||||
factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
@override $SnFilePoolCopyWith<$Res>? get pool;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -569,14 +583,15 @@ class __$SnCloudFileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnCloudFile(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,fileMeta: freezed == fileMeta ? _self._fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
|
||||
as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
|
||||
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||
@@ -589,7 +604,19 @@ as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFilePoolCopyWith<$Res>? get pool {
|
||||
if (_self.pool == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) {
|
||||
return _then(_self.copyWith(pool: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@@ -33,6 +33,10 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
|
||||
description: json['description'] as String?,
|
||||
fileMeta: json['file_meta'] as Map<String, dynamic>?,
|
||||
userMeta: json['user_meta'] as Map<String, dynamic>?,
|
||||
pool:
|
||||
json['pool'] == null
|
||||
? null
|
||||
: SnFilePool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||
sensitiveMarks:
|
||||
(json['sensitive_marks'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
@@ -61,6 +65,7 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
|
||||
'description': instance.description,
|
||||
'file_meta': instance.fileMeta,
|
||||
'user_meta': instance.userMeta,
|
||||
'pool': instance.pool?.toJson(),
|
||||
'sensitive_marks': instance.sensitiveMarks,
|
||||
'mime_type': instance.mimeType,
|
||||
'hash': instance.hash,
|
||||
|
25
lib/models/file_pool.dart
Normal file
25
lib/models/file_pool.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'file_pool.freezed.dart';
|
||||
part 'file_pool.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnFilePool with _$SnFilePool {
|
||||
const factory SnFilePool({
|
||||
required String id,
|
||||
required String name,
|
||||
String? description,
|
||||
Map<String, dynamic>? storageConfig,
|
||||
Map<String, dynamic>? billingConfig,
|
||||
Map<String, dynamic>? policyConfig,
|
||||
bool? isHidden,
|
||||
String? accountId,
|
||||
String? resourceIdentifier,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? deletedAt,
|
||||
}) = _SnFilePool;
|
||||
|
||||
factory SnFilePool.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFilePoolFromJson(json);
|
||||
}
|
328
lib/models/file_pool.freezed.dart
Normal file
328
lib/models/file_pool.freezed.dart
Normal file
@@ -0,0 +1,328 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'file_pool.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFilePool {
|
||||
|
||||
String get id; String get name; String? get description; Map<String, dynamic>? get storageConfig; Map<String, dynamic>? get billingConfig; Map<String, dynamic>? get policyConfig; bool? get isHidden; String? get accountId; String? get resourceIdentifier; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFilePoolCopyWith<SnFilePool> get copyWith => _$SnFilePoolCopyWithImpl<SnFilePool>(this as SnFilePool, _$identity);
|
||||
|
||||
/// Serializes this SnFilePool to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.storageConfig, storageConfig)&&const DeepCollectionEquality().equals(other.billingConfig, billingConfig)&&const DeepCollectionEquality().equals(other.policyConfig, policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(storageConfig),const DeepCollectionEquality().hash(billingConfig),const DeepCollectionEquality().hash(policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFilePoolCopyWith<$Res> {
|
||||
factory $SnFilePoolCopyWith(SnFilePool value, $Res Function(SnFilePool) _then) = _$SnFilePoolCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnFilePoolCopyWithImpl<$Res>
|
||||
implements $SnFilePoolCopyWith<$Res> {
|
||||
_$SnFilePoolCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFilePool _self;
|
||||
final $Res Function(SnFilePool) _then;
|
||||
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,storageConfig: freezed == storageConfig ? _self.storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self.billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self.policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnFilePool].
|
||||
extension SnFilePoolPatterns on SnFilePool {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnFilePool value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnFilePool value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnFilePool value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool():
|
||||
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnFilePool implements SnFilePool {
|
||||
const _SnFilePool({required this.id, required this.name, this.description, final Map<String, dynamic>? storageConfig, final Map<String, dynamic>? billingConfig, final Map<String, dynamic>? policyConfig, this.isHidden, this.accountId, this.resourceIdentifier, this.createdAt, this.updatedAt, this.deletedAt}): _storageConfig = storageConfig,_billingConfig = billingConfig,_policyConfig = policyConfig;
|
||||
factory _SnFilePool.fromJson(Map<String, dynamic> json) => _$SnFilePoolFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String name;
|
||||
@override final String? description;
|
||||
final Map<String, dynamic>? _storageConfig;
|
||||
@override Map<String, dynamic>? get storageConfig {
|
||||
final value = _storageConfig;
|
||||
if (value == null) return null;
|
||||
if (_storageConfig is EqualUnmodifiableMapView) return _storageConfig;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
final Map<String, dynamic>? _billingConfig;
|
||||
@override Map<String, dynamic>? get billingConfig {
|
||||
final value = _billingConfig;
|
||||
if (value == null) return null;
|
||||
if (_billingConfig is EqualUnmodifiableMapView) return _billingConfig;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
final Map<String, dynamic>? _policyConfig;
|
||||
@override Map<String, dynamic>? get policyConfig {
|
||||
final value = _policyConfig;
|
||||
if (value == null) return null;
|
||||
if (_policyConfig is EqualUnmodifiableMapView) return _policyConfig;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final bool? isHidden;
|
||||
@override final String? accountId;
|
||||
@override final String? resourceIdentifier;
|
||||
@override final DateTime? createdAt;
|
||||
@override final DateTime? updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFilePoolCopyWith<_SnFilePool> get copyWith => __$SnFilePoolCopyWithImpl<_SnFilePool>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFilePoolToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._storageConfig, _storageConfig)&&const DeepCollectionEquality().equals(other._billingConfig, _billingConfig)&&const DeepCollectionEquality().equals(other._policyConfig, _policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_storageConfig),const DeepCollectionEquality().hash(_billingConfig),const DeepCollectionEquality().hash(_policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFilePoolCopyWith<$Res> implements $SnFilePoolCopyWith<$Res> {
|
||||
factory _$SnFilePoolCopyWith(_SnFilePool value, $Res Function(_SnFilePool) _then) = __$SnFilePoolCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnFilePoolCopyWithImpl<$Res>
|
||||
implements _$SnFilePoolCopyWith<$Res> {
|
||||
__$SnFilePoolCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFilePool _self;
|
||||
final $Res Function(_SnFilePool) _then;
|
||||
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnFilePool(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,storageConfig: freezed == storageConfig ? _self._storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self._billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self._policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
47
lib/models/file_pool.g.dart
Normal file
47
lib/models/file_pool.g.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_pool.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnFilePool _$SnFilePoolFromJson(Map<String, dynamic> json) => _SnFilePool(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
storageConfig: json['storage_config'] as Map<String, dynamic>?,
|
||||
billingConfig: json['billing_config'] as Map<String, dynamic>?,
|
||||
policyConfig: json['policy_config'] as Map<String, dynamic>?,
|
||||
isHidden: json['is_hidden'] as bool?,
|
||||
accountId: json['account_id'] as String?,
|
||||
resourceIdentifier: json['resource_identifier'] as String?,
|
||||
createdAt:
|
||||
json['created_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt:
|
||||
json['updated_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnFilePoolToJson(_SnFilePool instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'storage_config': instance.storageConfig,
|
||||
'billing_config': instance.billingConfig,
|
||||
'policy_config': instance.policyConfig,
|
||||
'is_hidden': instance.isHidden,
|
||||
'account_id': instance.accountId,
|
||||
'resource_identifier': instance.resourceIdentifier,
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
@@ -4,12 +4,16 @@ import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/account/status.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
// Conditional imports for IPC server - use web stubs on web platform
|
||||
import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart';
|
||||
|
||||
const String kRpcLogPrefix = 'arRPC.websocket';
|
||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||
@@ -43,14 +47,14 @@ class IpcErrorCodes {
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
|
||||
class ActivityRpcServer {
|
||||
static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
|
||||
Map<String, Function>
|
||||
handlers; // {connection: (socket), message: (socket, data), close: (socket)}
|
||||
HttpServer? _httpServer;
|
||||
ServerSocket? _ipcServer;
|
||||
IpcServer? _ipcServer;
|
||||
final List<WebSocketChannel> _wsSockets = [];
|
||||
final List<_IpcSocketWrapper> _ipcSockets = [];
|
||||
|
||||
ActivityRpcServer(this.handlers);
|
||||
|
||||
@@ -58,109 +62,20 @@ class ActivityRpcServer {
|
||||
handlers = newHandlers;
|
||||
}
|
||||
|
||||
// Encode IPC packet
|
||||
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
|
||||
final jsonData = jsonEncode(data);
|
||||
final dataBytes = utf8.encode(jsonData);
|
||||
final dataSize = dataBytes.length;
|
||||
|
||||
final buffer = ByteData(8 + dataSize);
|
||||
buffer.setInt32(0, type, Endian.little);
|
||||
buffer.setInt32(4, dataSize, Endian.little);
|
||||
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
|
||||
|
||||
return buffer.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Future<String> _getMacOsSystemTmpDir() async {
|
||||
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
// Find available IPC socket path
|
||||
Future<String> _findAvailableIpcPath() async {
|
||||
// Build list of directories to try, with macOS-specific handling
|
||||
final baseDirs = <String>[];
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final macTempDir = await _getMacOsSystemTmpDir();
|
||||
if (macTempDir.isNotEmpty) {
|
||||
baseDirs.add(macTempDir);
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to get macOS system temp dir: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add other standard directories
|
||||
final otherDirs = [
|
||||
Platform.environment['XDG_RUNTIME_DIR'], // User runtime directory
|
||||
Platform.environment['TMPDIR'], // App container temp (fallback)
|
||||
Platform.environment['TMP'],
|
||||
Platform.environment['TEMP'],
|
||||
'/tmp', // System temp directory - most compatible
|
||||
];
|
||||
|
||||
baseDirs.addAll(
|
||||
otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
|
||||
);
|
||||
|
||||
for (final baseDir in baseDirs) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final socketPath = path.join(baseDir, '$kIpcBasePath-$i');
|
||||
try {
|
||||
final socket = await ServerSocket.bind(
|
||||
InternetAddress(socketPath, type: InternetAddressType.unix),
|
||||
0,
|
||||
);
|
||||
socket.close();
|
||||
// Clean up the test socket
|
||||
try {
|
||||
await File(socketPath).delete();
|
||||
} catch (_) {}
|
||||
developer.log(
|
||||
'IPC socket will be created at: $socketPath',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
return socketPath;
|
||||
} catch (e) {
|
||||
// Path not available, try next
|
||||
if (i == 0) {
|
||||
// Log only for the first attempt per directory
|
||||
developer.log(
|
||||
'IPC path $socketPath not available: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception(
|
||||
'No available IPC socket paths found in any temp directory',
|
||||
);
|
||||
}
|
||||
|
||||
// Start the WebSocket server
|
||||
// Start the server
|
||||
Future<void> start() async {
|
||||
int port = portRange[0];
|
||||
bool wsSuccess = false;
|
||||
|
||||
// Start WebSocket server
|
||||
while (port <= portRange[1]) {
|
||||
developer.log('trying port $port', name: kRpcLogPrefix);
|
||||
developer.log('Trying port $port', name: kRpcLogPrefix);
|
||||
try {
|
||||
// Start HTTP server
|
||||
_httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
|
||||
developer.log('listening on $port', name: kRpcLogPrefix);
|
||||
developer.log('Listening on $port', name: kRpcLogPrefix);
|
||||
|
||||
// Handle WebSocket upgrades
|
||||
shelf_io.serveRequests(_httpServer!, (Request request) async {
|
||||
developer.log('new request', name: kRpcLogPrefix);
|
||||
developer.log('New request', name: kRpcLogPrefix);
|
||||
if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
|
||||
final handler = webSocketHandler((WebSocketChannel channel, _) {
|
||||
_wsSockets.add(channel);
|
||||
@@ -169,7 +84,7 @@ class ActivityRpcServer {
|
||||
return handler(request);
|
||||
}
|
||||
developer.log(
|
||||
'new request disposed due to not websocket',
|
||||
'New request disposed due to not websocket',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
return Response.notFound('Not a WebSocket request');
|
||||
@@ -178,12 +93,12 @@ class ActivityRpcServer {
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e is SocketException && e.osError?.errorCode == 98) {
|
||||
// EADDRINUSE
|
||||
developer.log('$port in use!', name: kRpcLogPrefix);
|
||||
} else {
|
||||
developer.log('http error: $e', name: kRpcLogPrefix);
|
||||
developer.log('HTTP error: $e', name: kRpcLogPrefix);
|
||||
}
|
||||
port++;
|
||||
await Future.delayed(Duration(milliseconds: 100)); // Add delay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,27 +108,24 @@ class ActivityRpcServer {
|
||||
);
|
||||
}
|
||||
|
||||
// Start IPC server (skip on macOS due to sandboxing)
|
||||
final shouldStartIpc = !Platform.isMacOS;
|
||||
// Start IPC server
|
||||
final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
|
||||
if (shouldStartIpc) {
|
||||
try {
|
||||
final ipcPath = await _findAvailableIpcPath();
|
||||
_ipcServer = await ServerSocket.bind(
|
||||
InternetAddress(ipcPath, type: InternetAddressType.unix),
|
||||
0,
|
||||
);
|
||||
developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
|
||||
_ipcServer = MultiPlatformIpcServer();
|
||||
|
||||
_ipcServer!.listen((Socket socket) {
|
||||
_onIpcConnection(socket);
|
||||
});
|
||||
// Set up IPC handlers
|
||||
_ipcServer!.handlePacket = (socket, packet, _) {
|
||||
_handleIpcPacket(socket, packet);
|
||||
};
|
||||
|
||||
await _ipcServer!.start();
|
||||
} catch (e) {
|
||||
developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
|
||||
// Continue without IPC if it fails
|
||||
}
|
||||
} else {
|
||||
developer.log(
|
||||
'IPC server disabled on macOS in production mode due to sandboxing',
|
||||
'IPC server disabled on macOS or web in production mode',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
@@ -223,24 +135,23 @@ class ActivityRpcServer {
|
||||
Future<void> stop() async {
|
||||
// Stop WebSocket server
|
||||
for (var socket in _wsSockets) {
|
||||
await socket.sink.close();
|
||||
try {
|
||||
await socket.sink.close();
|
||||
} catch (e) {
|
||||
developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
|
||||
}
|
||||
}
|
||||
_wsSockets.clear();
|
||||
await _httpServer?.close();
|
||||
await _httpServer?.close(force: true);
|
||||
|
||||
// Stop IPC server
|
||||
for (var socket in _ipcSockets) {
|
||||
socket.close();
|
||||
}
|
||||
_ipcSockets.clear();
|
||||
await _ipcServer?.close();
|
||||
await _ipcServer?.stop();
|
||||
|
||||
developer.log('servers stopped', name: kRpcLogPrefix);
|
||||
developer.log('Servers stopped', name: kRpcLogPrefix);
|
||||
}
|
||||
|
||||
// Handle new WebSocket connection
|
||||
void _onWsConnection(WebSocketChannel socket, Request request) {
|
||||
// Parse query parameters
|
||||
final uri = request.url;
|
||||
final params = uri.queryParameters;
|
||||
final ver = int.tryParse(params['v'] ?? '1') ?? 1;
|
||||
@@ -249,43 +160,38 @@ class ActivityRpcServer {
|
||||
final origin = request.headers['origin'] ?? '';
|
||||
|
||||
developer.log(
|
||||
'new WS connection! origin: $origin, params: $params',
|
||||
'New WS connection! origin: $origin, params: $params',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
|
||||
// Validate origin
|
||||
if (origin.isNotEmpty &&
|
||||
![
|
||||
'https://discord.com',
|
||||
'https://ptb.discord.com',
|
||||
'https://canary.discord.com',
|
||||
].contains(origin)) {
|
||||
developer.log('disallowed origin: $origin', name: kRpcLogPrefix);
|
||||
developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate encoding
|
||||
if (encoding != 'json') {
|
||||
developer.log(
|
||||
'unsupported encoding requested: $encoding',
|
||||
'Unsupported encoding requested: $encoding',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate version
|
||||
if (ver != 1) {
|
||||
developer.log('unsupported version requested: $ver', name: kRpcLogPrefix);
|
||||
developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store client info on socket
|
||||
final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
|
||||
|
||||
// Set up event listeners
|
||||
socket.stream.listen(
|
||||
(data) => _onWsMessage(socketWithMeta, data),
|
||||
onError: (e) {
|
||||
@@ -298,36 +204,27 @@ class ActivityRpcServer {
|
||||
},
|
||||
);
|
||||
|
||||
// Notify handler of new connection
|
||||
handlers['connection']?.call(socketWithMeta);
|
||||
}
|
||||
|
||||
// Handle new IPC connection
|
||||
void _onIpcConnection(Socket socket) {
|
||||
developer.log('new IPC connection!', name: kRpcIpcLogPrefix);
|
||||
|
||||
final socketWrapper = _IpcSocketWrapper(socket);
|
||||
_ipcSockets.add(socketWrapper);
|
||||
|
||||
// Set up event listeners
|
||||
socket.listen(
|
||||
(data) => _onIpcData(socketWrapper, data),
|
||||
onError: (e) {
|
||||
developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.close();
|
||||
},
|
||||
onDone: () {
|
||||
developer.log('IPC socket closed', name: kRpcIpcLogPrefix);
|
||||
handlers['close']?.call(socketWrapper);
|
||||
_ipcSockets.remove(socketWrapper);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Handle incoming WebSocket message
|
||||
void _onWsMessage(_WsSocketWrapper socket, dynamic data) {
|
||||
Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
|
||||
if (data is! String) {
|
||||
developer.log(
|
||||
'Invalid WebSocket message: not a string',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final jsonData = jsonDecode(data as String);
|
||||
final jsonData = await compute(jsonDecode, data);
|
||||
if (jsonData is! Map<String, dynamic>) {
|
||||
developer.log(
|
||||
'Invalid WebSocket message: not a JSON object',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
return;
|
||||
}
|
||||
developer.log('WS message: $jsonData', name: kRpcLogPrefix);
|
||||
handlers['message']?.call(socket, jsonData);
|
||||
} catch (e) {
|
||||
@@ -335,22 +232,8 @@ class ActivityRpcServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming IPC data
|
||||
void _onIpcData(_IpcSocketWrapper socket, List<int> data) {
|
||||
try {
|
||||
socket.addData(data);
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
_handleIpcPacket(socket, packet);
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IPC packet
|
||||
void _handleIpcPacket(_IpcSocketWrapper socket, _IpcPacket packet) {
|
||||
void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
|
||||
switch (packet.type) {
|
||||
case IpcTypes.ping:
|
||||
developer.log('IPC ping received', name: kRpcIpcLogPrefix);
|
||||
@@ -359,7 +242,6 @@ class ActivityRpcServer {
|
||||
|
||||
case IpcTypes.pong:
|
||||
developer.log('IPC pong received', name: kRpcIpcLogPrefix);
|
||||
// Handle pong if needed
|
||||
break;
|
||||
|
||||
case IpcTypes.handshake:
|
||||
@@ -388,13 +270,12 @@ class ActivityRpcServer {
|
||||
}
|
||||
|
||||
// Handle IPC handshake
|
||||
void _onIpcHandshake(_IpcSocketWrapper socket, Map<String, dynamic> params) {
|
||||
void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
|
||||
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
|
||||
|
||||
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
||||
final clientId = params['client_id']?.toString() ?? '';
|
||||
|
||||
// Validate version
|
||||
if (ver != 1) {
|
||||
developer.log(
|
||||
'IPC unsupported version requested: $ver',
|
||||
@@ -404,7 +285,6 @@ class ActivityRpcServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate client ID
|
||||
if (clientId.isEmpty) {
|
||||
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
||||
@@ -413,7 +293,6 @@ class ActivityRpcServer {
|
||||
|
||||
socket.clientId = clientId;
|
||||
|
||||
// Notify handler of new connection
|
||||
handlers['connection']?.call(socket);
|
||||
}
|
||||
}
|
||||
@@ -432,74 +311,6 @@ class _WsSocketWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
// IPC wrapper
|
||||
class _IpcSocketWrapper {
|
||||
final Socket socket;
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
final List<int> _buffer = [];
|
||||
|
||||
_IpcSocketWrapper(this.socket);
|
||||
|
||||
void addData(List<int> data) {
|
||||
_buffer.addAll(data);
|
||||
}
|
||||
|
||||
void send(Map<String, dynamic> msg) {
|
||||
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
|
||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
void sendPong(dynamic data) {
|
||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
void close() {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
void closeWithCode(int code, [String message = '']) {
|
||||
final closeData = {'code': code, 'message': message};
|
||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.close, closeData);
|
||||
socket.add(packet);
|
||||
socket.close();
|
||||
}
|
||||
|
||||
List<_IpcPacket> readPackets() {
|
||||
final packets = <_IpcPacket>[];
|
||||
|
||||
while (_buffer.length >= 8) {
|
||||
final buffer = Uint8List.fromList(_buffer);
|
||||
final byteData = ByteData.view(buffer.buffer);
|
||||
|
||||
final type = byteData.getInt32(0, Endian.little);
|
||||
final dataSize = byteData.getInt32(4, Endian.little);
|
||||
|
||||
if (_buffer.length < 8 + dataSize) break;
|
||||
|
||||
final dataBytes = _buffer.sublist(8, 8 + dataSize);
|
||||
final jsonStr = utf8.decode(dataBytes);
|
||||
final jsonData = jsonDecode(jsonStr);
|
||||
|
||||
packets.add(_IpcPacket(type, jsonData));
|
||||
|
||||
_buffer.removeRange(0, 8 + dataSize);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class _IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
_IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
// State management for server status and activities
|
||||
class ServerState {
|
||||
final String status;
|
||||
@@ -522,7 +333,6 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||
: super(ServerState(status: 'Server not started'));
|
||||
|
||||
Future<void> start() async {
|
||||
// Only start server on desktop platforms
|
||||
if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
|
||||
try {
|
||||
await server.start();
|
||||
@@ -531,7 +341,9 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||
state = state.copyWith(status: 'Server failed: $e');
|
||||
}
|
||||
} else {
|
||||
state = state.copyWith(status: 'Server disabled on mobile/web');
|
||||
Future(() {
|
||||
state = state.copyWith(status: 'Server disabled on mobile/web');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,9 +366,8 @@ final rpcServerStateProvider =
|
||||
final clientId =
|
||||
socket is _WsSocketWrapper
|
||||
? socket.clientId
|
||||
: (socket as _IpcSocketWrapper).clientId;
|
||||
: (socket as IpcSocketWrapper).clientId;
|
||||
notifier.updateStatus('Client connected (ID: $clientId)');
|
||||
// Send READY event
|
||||
socket.send({
|
||||
'cmd': 'DISPATCH',
|
||||
'data': {
|
||||
@@ -575,26 +386,47 @@ final rpcServerStateProvider =
|
||||
},
|
||||
},
|
||||
'evt': 'READY',
|
||||
'nonce': '12345', // Should be dynamic
|
||||
'nonce': '12345',
|
||||
});
|
||||
},
|
||||
'message': (socket, dynamic data) async {
|
||||
if (data['cmd'] == 'SET_ACTIVITY') {
|
||||
notifier.addActivity(
|
||||
'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
|
||||
'Activity: ${data['args']['activity']['details'] ?? ''}',
|
||||
);
|
||||
// Call setRemoteActivityStatus
|
||||
final label = data['args']['activity']['details'] ?? 'Unknown';
|
||||
final label = data['args']['activity']['details'] ?? '';
|
||||
final appId = socket.clientId;
|
||||
final meta = data['args']['activity'];
|
||||
try {
|
||||
await setRemoteActivityStatus(ref, label, appId);
|
||||
await setRemoteActivityStatus(
|
||||
ref,
|
||||
label,
|
||||
appId,
|
||||
meta,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final status = SnAccountStatus(
|
||||
id: 'local_$appId',
|
||||
attitude: 0,
|
||||
isOnline: true,
|
||||
isInvisible: false,
|
||||
isNotDisturb: false,
|
||||
isCustomized: true,
|
||||
label: label,
|
||||
meta: meta,
|
||||
clearedAt: null,
|
||||
accountId: 'me',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
);
|
||||
ref.read(currentAccountStatusProvider.notifier).setStatus(status);
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to set remote activity status: $e',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
}
|
||||
// Echo back success
|
||||
socket.send({
|
||||
'cmd': 'SET_ACTIVITY',
|
||||
'data': data['args']['activity'],
|
||||
@@ -608,6 +440,7 @@ final rpcServerStateProvider =
|
||||
final appId = socket.clientId;
|
||||
try {
|
||||
await unsetRemoteActivityStatus(ref, appId);
|
||||
ref.read(currentAccountStatusProvider.notifier).clearStatus();
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to unset remote activity status: $e',
|
||||
@@ -628,6 +461,7 @@ Future<void> setRemoteActivityStatus(
|
||||
Ref ref,
|
||||
String label,
|
||||
String appId,
|
||||
Map<String, dynamic> meta,
|
||||
) async {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post(
|
||||
@@ -638,6 +472,7 @@ Future<void> setRemoteActivityStatus(
|
||||
'is_automated': true,
|
||||
'label': label,
|
||||
'app_identifier': appId,
|
||||
'meta': meta,
|
||||
},
|
||||
);
|
||||
}
|
297
lib/pods/activity/ipc_server.dart
Normal file
297
lib/pods/activity/ipc_server.dart
Normal file
@@ -0,0 +1,297 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dart_ipc/dart_ipc.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||
|
||||
// IPC Packet Types
|
||||
class IpcTypes {
|
||||
static const int handshake = 0;
|
||||
static const int frame = 1;
|
||||
static const int close = 2;
|
||||
static const int ping = 3;
|
||||
static const int pong = 4;
|
||||
}
|
||||
|
||||
// IPC Close Codes
|
||||
class IpcCloseCodes {
|
||||
static const int closeNormal = 1000;
|
||||
static const int closeUnsupported = 1003;
|
||||
static const int closeAbnormal = 1006;
|
||||
}
|
||||
|
||||
// IPC Error Codes
|
||||
class IpcErrorCodes {
|
||||
static const int invalidClientId = 4000;
|
||||
static const int invalidOrigin = 4001;
|
||||
static const int rateLimited = 4002;
|
||||
static const int tokenRevoked = 4003;
|
||||
static const int invalidVersion = 4004;
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
// Abstract base class for IPC server
|
||||
abstract class IpcServer {
|
||||
final List<IpcSocketWrapper> _sockets = [];
|
||||
|
||||
// Encode IPC packet
|
||||
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
|
||||
final jsonData = jsonEncode(data);
|
||||
final dataBytes = utf8.encode(jsonData);
|
||||
final dataSize = dataBytes.length;
|
||||
|
||||
final buffer = ByteData(8 + dataSize);
|
||||
buffer.setInt32(0, type, Endian.little);
|
||||
buffer.setInt32(4, dataSize, Endian.little);
|
||||
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
|
||||
|
||||
return buffer.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Future<void> start();
|
||||
Future<void> stop();
|
||||
|
||||
void addSocket(IpcSocketWrapper socket) {
|
||||
_sockets.add(socket);
|
||||
}
|
||||
|
||||
void removeSocket(IpcSocketWrapper socket) {
|
||||
_sockets.remove(socket);
|
||||
}
|
||||
|
||||
List<IpcSocketWrapper> get sockets => _sockets;
|
||||
|
||||
void Function(
|
||||
IpcSocketWrapper socket,
|
||||
IpcPacket packet,
|
||||
Map<String, Function> handlers,
|
||||
)?
|
||||
handlePacket;
|
||||
}
|
||||
|
||||
// Abstract base class for IPC socket wrapper
|
||||
abstract class IpcSocketWrapper {
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
final List<int> _buffer = [];
|
||||
|
||||
void addData(List<int> data) {
|
||||
_buffer.addAll(data);
|
||||
}
|
||||
|
||||
void send(Map<String, dynamic> msg);
|
||||
void sendPong(dynamic data);
|
||||
void close();
|
||||
void closeWithCode(int code, [String message = '']);
|
||||
|
||||
List<IpcPacket> readPackets() {
|
||||
final packets = <IpcPacket>[];
|
||||
|
||||
while (_buffer.length >= 8) {
|
||||
final buffer = Uint8List.fromList(_buffer);
|
||||
final byteData = ByteData.view(buffer.buffer);
|
||||
|
||||
final type = byteData.getInt32(0, Endian.little);
|
||||
final dataSize = byteData.getInt32(4, Endian.little);
|
||||
|
||||
if (_buffer.length < 8 + dataSize) break;
|
||||
|
||||
final dataBytes = _buffer.sublist(8, 8 + dataSize);
|
||||
final jsonStr = utf8.decode(dataBytes);
|
||||
final jsonData = jsonDecode(jsonStr);
|
||||
|
||||
packets.add(IpcPacket(type, jsonData));
|
||||
|
||||
_buffer.removeRange(0, 8 + dataSize);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiplatform IPC Server implementation using dart_ipc
|
||||
class MultiPlatformIpcServer extends IpcServer {
|
||||
StreamSubscription? _serverSubscription;
|
||||
|
||||
@override
|
||||
Future<void> start() async {
|
||||
try {
|
||||
final ipcPath = Platform.isWindows
|
||||
? r'\\.\pipe\discord-ipc-0'
|
||||
: await _findAvailableUnixIpcPath();
|
||||
|
||||
final serverSocket = await bind(ipcPath);
|
||||
developer.log(
|
||||
'IPC listening at $ipcPath',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
|
||||
_serverSubscription = serverSocket.listen((socket) {
|
||||
final socketWrapper = MultiPlatformIpcSocketWrapper(socket);
|
||||
addSocket(socketWrapper);
|
||||
developer.log(
|
||||
'New IPC connection!',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
_handleIpcData(socketWrapper);
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to start IPC server: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
for (var socket in sockets) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (e) {
|
||||
developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
|
||||
}
|
||||
}
|
||||
sockets.clear();
|
||||
_serverSubscription?.cancel();
|
||||
}
|
||||
|
||||
// Handle incoming IPC data
|
||||
void _handleIpcData(MultiPlatformIpcSocketWrapper socket) {
|
||||
final startTime = DateTime.now();
|
||||
socket.socket.listen((data) {
|
||||
final readStart = DateTime.now();
|
||||
socket.addData(data);
|
||||
final readDuration = DateTime.now().difference(readStart).inMicroseconds;
|
||||
developer.log(
|
||||
'Read data took $readDuration microseconds',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
handlePacket?.call(socket, packet, {});
|
||||
}
|
||||
}, onDone: () {
|
||||
developer.log('IPC connection closed', name: kRpcIpcLogPrefix);
|
||||
socket.close();
|
||||
}, onError: (e) {
|
||||
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
});
|
||||
final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
|
||||
developer.log(
|
||||
'_handleIpcData took $totalDuration microseconds',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getMacOsSystemTmpDir() async {
|
||||
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
// Find available IPC socket path for Unix-like systems
|
||||
Future<String> _findAvailableUnixIpcPath() async {
|
||||
// Build list of directories to try, with macOS-specific handling
|
||||
final baseDirs = <String>[];
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final macTempDir = await _getMacOsSystemTmpDir();
|
||||
if (macTempDir.isNotEmpty) {
|
||||
baseDirs.add(macTempDir);
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to get macOS system temp dir: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add other standard directories
|
||||
final otherDirs = [
|
||||
Platform.environment['XDG_RUNTIME_DIR'],
|
||||
Platform.environment['TMPDIR'],
|
||||
Platform.environment['TMP'],
|
||||
Platform.environment['TEMP'],
|
||||
'/tmp',
|
||||
];
|
||||
|
||||
baseDirs.addAll(
|
||||
otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
|
||||
);
|
||||
|
||||
for (final baseDir in baseDirs) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final socketPath = path.join(baseDir, 'discord-ipc-$i');
|
||||
try {
|
||||
final socket = await bind(socketPath);
|
||||
socket.close();
|
||||
try {
|
||||
await File(socketPath).delete();
|
||||
} catch (_) {}
|
||||
developer.log(
|
||||
'IPC socket will be created at: $socketPath',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
return socketPath;
|
||||
} catch (e) {
|
||||
if (i == 0) {
|
||||
developer.log(
|
||||
'IPC path $socketPath not available: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception(
|
||||
'No available IPC socket paths found in any temp directory',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Multiplatform IPC Socket Wrapper
|
||||
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {
|
||||
final dynamic socket;
|
||||
|
||||
MultiPlatformIpcSocketWrapper(this.socket);
|
||||
|
||||
@override
|
||||
void send(Map<String, dynamic> msg) {
|
||||
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendPong(dynamic data) {
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void closeWithCode(int code, [String message = '']) {
|
||||
final closeData = {'code': code, 'message': message};
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
|
||||
socket.add(packet);
|
||||
socket.close();
|
||||
}
|
||||
}
|
61
lib/pods/activity/ipc_server.web.dart
Normal file
61
lib/pods/activity/ipc_server.web.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// Stub implementation for web platform
|
||||
// This file provides empty implementations to avoid import errors on web
|
||||
|
||||
// IPC Packet Types
|
||||
class IpcTypes {
|
||||
static const int handshake = 0;
|
||||
static const int frame = 1;
|
||||
static const int close = 2;
|
||||
static const int ping = 3;
|
||||
static const int pong = 4;
|
||||
}
|
||||
|
||||
// IPC Close Codes
|
||||
class IpcCloseCodes {
|
||||
static const int closeNormal = 1000;
|
||||
static const int closeUnsupported = 1003;
|
||||
static const int closeAbnormal = 1006;
|
||||
}
|
||||
|
||||
// IPC Error Codes
|
||||
class IpcErrorCodes {
|
||||
static const int invalidClientId = 4000;
|
||||
static const int invalidOrigin = 4001;
|
||||
static const int rateLimited = 4002;
|
||||
static const int tokenRevoked = 4003;
|
||||
static const int invalidVersion = 4004;
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
class IpcServer {
|
||||
Future<void> start() async {}
|
||||
Future<void> stop() async {}
|
||||
void Function(dynamic, dynamic, dynamic)? handlePacket;
|
||||
void addSocket(dynamic socket) {}
|
||||
void removeSocket(dynamic socket) {}
|
||||
List<dynamic> get sockets => [];
|
||||
}
|
||||
|
||||
class IpcSocketWrapper {
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
|
||||
void addData(List<int> data) {}
|
||||
void send(Map<String, dynamic> msg) {}
|
||||
void sendPong(dynamic data) {}
|
||||
void close() {}
|
||||
void closeWithCode(int code, [String message = '']) {}
|
||||
List<dynamic> readPackets() => [];
|
||||
}
|
||||
|
||||
class MultiPlatformIpcServer extends IpcServer {}
|
||||
|
||||
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {}
|
34
lib/pods/chat_rooms.dart
Normal file
34
lib/pods/chat_rooms.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import "dart:async";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
|
||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||
|
||||
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||
final controller = StreamController<AppLifecycleState>();
|
||||
|
||||
final observer = _AppLifecycleObserver((state) {
|
||||
if (controller.isClosed) return;
|
||||
controller.add(state);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(observer);
|
||||
|
||||
ref.onDispose(() {
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
controller.close();
|
||||
});
|
||||
|
||||
return controller.stream;
|
||||
});
|
||||
|
||||
class _AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
final ValueChanged<AppLifecycleState> onChange;
|
||||
_AppLifecycleObserver(this.onChange);
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onChange(state);
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/pods/theme.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
part 'config.freezed.dart';
|
||||
part 'config.g.dart';
|
||||
@@ -24,7 +25,10 @@ const kAppDataSavingMode = 'app_data_saving_mode';
|
||||
const kAppSoundEffects = 'app_sound_effects';
|
||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||
const kAppWindowSize = 'app_window_size';
|
||||
const kAppWindowOpacity = 'app_window_opacity';
|
||||
const kAppEnterToSend = 'app_enter_to_send';
|
||||
const kAppDefaultPoolId = 'app_default_pool_id';
|
||||
const kAppMessageDisplayStyle = 'app_message_display_style';
|
||||
const kFeaturedPostsCollapsedId =
|
||||
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
||||
|
||||
@@ -65,6 +69,9 @@ sealed class AppSettings with _$AppSettings {
|
||||
required String? customFonts,
|
||||
required int? appColorScheme, // The color stored via the int type
|
||||
required Size? windowSize, // The window size for desktop platforms
|
||||
required double windowOpacity, // The window opacity for desktop platforms
|
||||
required String? defaultPoolId,
|
||||
required String messageDisplayStyle,
|
||||
}) = _AppSettings;
|
||||
}
|
||||
|
||||
@@ -84,6 +91,9 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
customFonts: prefs.getString(kAppCustomFonts),
|
||||
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
|
||||
windowSize: _getWindowSizeFromPrefs(prefs),
|
||||
windowOpacity: prefs.getDouble(kAppWindowOpacity) ?? 1.0,
|
||||
defaultPoolId: prefs.getString(kAppDefaultPoolId),
|
||||
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,13 +114,23 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
void setDefaultPoolId(String? value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
if (value != null) {
|
||||
prefs.setString(kAppDefaultPoolId, value);
|
||||
} else {
|
||||
prefs.remove(kAppDefaultPoolId);
|
||||
}
|
||||
state = state.copyWith(defaultPoolId: value);
|
||||
}
|
||||
|
||||
void setAutoTranslate(bool value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setBool(kAppAutoTranslate, value);
|
||||
state = state.copyWith(autoTranslate: value);
|
||||
}
|
||||
|
||||
void setDataSavingMode(bool value){
|
||||
void setDataSavingMode(bool value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setBool(kAppDataSavingMode, value);
|
||||
state = state.copyWith(dataSavingMode: value);
|
||||
@@ -174,6 +194,19 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
Size? getWindowSize() {
|
||||
return state.windowSize;
|
||||
}
|
||||
|
||||
void setMessageDisplayStyle(String value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setString(kAppMessageDisplayStyle, value);
|
||||
state = state.copyWith(messageDisplayStyle: value);
|
||||
}
|
||||
|
||||
void setWindowOpacity(double value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setDouble(kAppWindowOpacity, value);
|
||||
state = state.copyWith(windowOpacity: value);
|
||||
Future(() => windowManager.setOpacity(value));
|
||||
}
|
||||
}
|
||||
|
||||
final updateInfoProvider =
|
||||
|
@@ -15,7 +15,9 @@ T _$identity<T>(T value) => value;
|
||||
mixin _$AppSettings {
|
||||
|
||||
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
|
||||
Size? get windowSize;
|
||||
Size? get windowSize;// The window size for desktop platforms
|
||||
double get windowOpacity;// The window opacity for desktop platforms
|
||||
String? get defaultPoolId; String get messageDisplayStyle;
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -26,16 +28,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize);
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,windowOpacity,defaultPoolId,messageDisplayStyle);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)';
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, windowOpacity: $windowOpacity, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +48,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
|
||||
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +65,7 @@ class _$AppSettingsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||
@@ -75,7 +77,10 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI
|
||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,
|
||||
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
||||
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -157,10 +162,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings() when $default != null:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.defaultPoolId,_that.messageDisplayStyle);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -178,10 +183,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings():
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);}
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.defaultPoolId,_that.messageDisplayStyle);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -195,10 +200,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings() when $default != null:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.defaultPoolId,_that.messageDisplayStyle);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -210,7 +215,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
|
||||
|
||||
class _AppSettings implements AppSettings {
|
||||
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize});
|
||||
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.windowOpacity, required this.defaultPoolId, required this.messageDisplayStyle});
|
||||
|
||||
|
||||
@override final bool autoTranslate;
|
||||
@@ -224,6 +229,11 @@ class _AppSettings implements AppSettings {
|
||||
@override final int? appColorScheme;
|
||||
// The color stored via the int type
|
||||
@override final Size? windowSize;
|
||||
// The window size for desktop platforms
|
||||
@override final double windowOpacity;
|
||||
// The window opacity for desktop platforms
|
||||
@override final String? defaultPoolId;
|
||||
@override final String messageDisplayStyle;
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -235,16 +245,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize);
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,windowOpacity,defaultPoolId,messageDisplayStyle);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)';
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, windowOpacity: $windowOpacity, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
}
|
||||
|
||||
|
||||
@@ -255,7 +265,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
|
||||
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle
|
||||
});
|
||||
|
||||
|
||||
@@ -272,7 +282,7 @@ class __$AppSettingsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
return _then(_AppSettings(
|
||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||
@@ -284,7 +294,10 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI
|
||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,
|
||||
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
||||
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,7 @@ part of 'config.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$appSettingsNotifierHash() =>
|
||||
r'cd18bff2614a94e3523634e6c577cefad0367eba';
|
||||
r'b5e9b2ea9b01c236a68669a00eaa563c1fb4efa6';
|
||||
|
||||
/// See also [AppSettingsNotifier].
|
||||
@ProviderFor(AppSettingsNotifier)
|
||||
|
24
lib/pods/file_pool.dart
Normal file
24
lib/pods/file_pool.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
final poolsProvider = FutureProvider<List<SnFilePool>>((ref) async {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
final response = await dio.get('/drive/pools');
|
||||
return response.data
|
||||
.map((e) => SnFilePool.fromJson(e))
|
||||
.cast<SnFilePool>()
|
||||
.toList();
|
||||
});
|
||||
|
||||
String? resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) {
|
||||
final settings = ref.watch(appSettingsNotifierProvider);
|
||||
|
||||
final configuredId = settings.defaultPoolId;
|
||||
if (configuredId != null && pools.any((p) => p.id == configuredId)) {
|
||||
return configuredId;
|
||||
}
|
||||
|
||||
return pools.firstOrNull?.id;
|
||||
}
|
@@ -10,17 +10,19 @@ Future<void> resetDatabase(WidgetRef ref) async {
|
||||
if (kIsWeb) return;
|
||||
|
||||
final db = ref.read(databaseProvider);
|
||||
final basepath = await getApplicationSupportDirectory();
|
||||
final file = File(join(basepath.path, 'solar_network_data.sqlite'));
|
||||
|
||||
// Close current database connection
|
||||
db.close();
|
||||
await db.close();
|
||||
|
||||
// Delete database file
|
||||
// Get the correct database file path
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(join(dbFolder.path, 'solar_network_data.sqlite'));
|
||||
|
||||
// Delete database file if it exists
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
|
||||
// Force refresh the database provider
|
||||
// Force refresh the database provider to create a new instance
|
||||
ref.invalidate(databaseProvider);
|
||||
}
|
||||
|
852
lib/pods/messages_notifier.dart
Normal file
852
lib/pods/messages_notifier.dart
Normal file
@@ -0,0 +1,852 @@
|
||||
import "dart:async";
|
||||
import "dart:developer" as developer;
|
||||
import "package:dio/dio.dart";
|
||||
import "package:drift/drift.dart" show Variable;
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:island/database/drift_db.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/pods/database.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/file.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/pods/chat_rooms.dart";
|
||||
|
||||
part 'messages_notifier.g.dart';
|
||||
|
||||
@riverpod
|
||||
class MessagesNotifier extends _$MessagesNotifier {
|
||||
late final Dio _apiClient;
|
||||
late final AppDatabase _database;
|
||||
late final SnChatRoom _room;
|
||||
late final SnChatMember _identity;
|
||||
|
||||
final Map<String, LocalChatMessage> _pendingMessages = {};
|
||||
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
||||
int? _totalCount;
|
||||
String? _searchQuery;
|
||||
bool? _withLinks;
|
||||
bool? _withAttachments;
|
||||
|
||||
late final String _roomId;
|
||||
static const int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
bool _isSyncing = false;
|
||||
bool _isJumping = false;
|
||||
|
||||
@override
|
||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||
_roomId = roomId;
|
||||
_apiClient = ref.watch(apiClientProvider);
|
||||
_database = ref.watch(databaseProvider);
|
||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||
|
||||
if (room == null) {
|
||||
throw Exception('Room not found');
|
||||
}
|
||||
_room = room;
|
||||
|
||||
// Allow building even if identity is null for public rooms
|
||||
if (identity != null) {
|
||||
_identity = identity;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'MessagesNotifier built for room $roomId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
// Only setup sync and lifecycle listeners if user is a member
|
||||
if (identity != null) {
|
||||
ref.listen(appLifecycleStateProvider, (_, next) {
|
||||
if (next.hasValue && next.value == AppLifecycleState.resumed) {
|
||||
developer.log(
|
||||
'App resumed, syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
syncMessages();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadInitial();
|
||||
return [];
|
||||
}
|
||||
|
||||
List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) {
|
||||
messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return messages;
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> _getCachedMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
}) async {
|
||||
developer.log(
|
||||
'Getting cached messages from offset $offset, take $take',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final List<LocalChatMessage> dbMessages;
|
||||
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
|
||||
dbMessages = await _database.searchMessages(
|
||||
_roomId,
|
||||
_searchQuery ?? '',
|
||||
withAttachments: _withAttachments,
|
||||
);
|
||||
} else {
|
||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||
_roomId,
|
||||
offset: offset,
|
||||
limit: take,
|
||||
);
|
||||
dbMessages =
|
||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||
}
|
||||
|
||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||
|
||||
if (_withLinks == true) {
|
||||
filteredMessages =
|
||||
filteredMessages.where((msg) => _hasLink(msg)).toList();
|
||||
}
|
||||
|
||||
final dbLocalMessages = filteredMessages;
|
||||
|
||||
// Always ensure unique messages to prevent duplicate keys
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
final seenIds = <String>{};
|
||||
for (final message in dbLocalMessages) {
|
||||
if (seenIds.add(message.id)) {
|
||||
uniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
final pendingForRoom =
|
||||
_pendingMessages.values
|
||||
.where((msg) => msg.roomId == _roomId)
|
||||
.toList();
|
||||
|
||||
final allMessages = [...pendingForRoom, ...uniqueMessages];
|
||||
_sortMessages(allMessages); // Use the helper function
|
||||
|
||||
final finalUniqueMessages = <LocalChatMessage>[];
|
||||
final finalSeenIds = <String>{};
|
||||
for (final message in allMessages) {
|
||||
if (finalSeenIds.add(message.id)) {
|
||||
finalUniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
return finalUniqueMessages;
|
||||
}
|
||||
|
||||
return uniqueMessages;
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
}) async {
|
||||
developer.log(
|
||||
'Fetching messages from API, offset $offset, take $take',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
if (_totalCount == null) {
|
||||
final response = await _apiClient.get(
|
||||
'/sphere/chat/$_roomId/messages',
|
||||
queryParameters: {'offset': 0, 'take': 1},
|
||||
);
|
||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
||||
}
|
||||
|
||||
if (offset >= _totalCount!) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/sphere/chat/$_roomId/messages',
|
||||
queryParameters: {'offset': offset, 'take': take},
|
||||
);
|
||||
|
||||
final List<dynamic> data = response.data;
|
||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
||||
|
||||
final messages =
|
||||
data.map((json) {
|
||||
final remoteMessage = SnChatMessage.fromJson(json);
|
||||
return LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
for (final message in messages) {
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
if (message.nonce != null) {
|
||||
_pendingMessages.removeWhere(
|
||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
Future<void> syncMessages() async {
|
||||
if (_isSyncing) {
|
||||
developer.log(
|
||||
'Sync already in progress, skipping.',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_isSyncing = true;
|
||||
|
||||
developer.log('Starting message sync', name: 'MessagesNotifier');
|
||||
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||
try {
|
||||
final dbMessages = await _database.getMessagesForRoom(
|
||||
_room.id,
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
);
|
||||
final lastMessage =
|
||||
dbMessages.isEmpty
|
||||
? null
|
||||
: _database.companionToMessage(dbMessages.first);
|
||||
|
||||
if (lastMessage == null) {
|
||||
developer.log(
|
||||
'No local messages, fetching from network',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final newMessages = await _fetchAndCacheMessages(
|
||||
offset: 0,
|
||||
take: _pageSize,
|
||||
);
|
||||
state = AsyncValue.data(newMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
final resp = await _apiClient.post(
|
||||
'/sphere/chat/${_room.id}/sync',
|
||||
data: {
|
||||
'last_sync_timestamp':
|
||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
||||
},
|
||||
);
|
||||
|
||||
final response = MessageSyncResponse.fromJson(resp.data);
|
||||
developer.log(
|
||||
'Sync response: ${response.messages.length} changes',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
for (final message in response.messages) {
|
||||
switch (message.type) {
|
||||
case "messages.update":
|
||||
case "messages.update.links":
|
||||
await receiveMessageUpdate(message);
|
||||
break;
|
||||
case "messages.delete":
|
||||
await receiveMessageDeletion(message.id.toString());
|
||||
break;
|
||||
}
|
||||
// Still need receive the message to show the history actions
|
||||
await receiveMessage(message);
|
||||
}
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
'Error syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
developer.log('Finished message sync', name: 'MessagesNotifier');
|
||||
Future.microtask(
|
||||
() => ref.read(isSyncingProvider.notifier).state = false,
|
||||
);
|
||||
_isSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> listMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
bool synced = false,
|
||||
}) async {
|
||||
try {
|
||||
if (offset == 0 &&
|
||||
!synced &&
|
||||
(_searchQuery == null || _searchQuery!.isEmpty)) {
|
||||
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
|
||||
return <LocalChatMessage>[];
|
||||
});
|
||||
}
|
||||
|
||||
final localMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
);
|
||||
|
||||
if (localMessages.isNotEmpty) {
|
||||
return localMessages;
|
||||
}
|
||||
|
||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||
return await _fetchAndCacheMessages(offset: offset, take: take);
|
||||
} else {
|
||||
return []; // If searching, and no local messages, don't fetch from network
|
||||
}
|
||||
} catch (e) {
|
||||
final localMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
);
|
||||
|
||||
if (localMessages.isNotEmpty) {
|
||||
return localMessages;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadInitial() async {
|
||||
developer.log('Loading initial messages', name: 'MessagesNotifier');
|
||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||
syncMessages();
|
||||
}
|
||||
|
||||
final messages = await _getCachedMessages(offset: 0, take: _pageSize);
|
||||
|
||||
_hasMore = messages.length == _pageSize;
|
||||
|
||||
state = AsyncValue.data(messages);
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMore || state is AsyncLoading) return;
|
||||
developer.log('Loading more messages', name: 'MessagesNotifier');
|
||||
|
||||
try {
|
||||
final currentMessages = state.value ?? [];
|
||||
final offset = currentMessages.length;
|
||||
|
||||
final newMessages = await listMessages(offset: offset, take: _pageSize);
|
||||
|
||||
if (newMessages.isEmpty || newMessages.length < _pageSize) {
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
state = AsyncValue.data(
|
||||
_sortMessages([...currentMessages, ...newMessages]),
|
||||
);
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
'Error loading more messages',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
String content,
|
||||
List<UniversalFile> attachments, {
|
||||
SnChatMessage? editingTo,
|
||||
SnChatMessage? forwardingTo,
|
||||
SnChatMessage? replyingTo,
|
||||
Function(String, Map<int, double>)? onProgress,
|
||||
}) async {
|
||||
final nonce = const Uuid().v4();
|
||||
developer.log(
|
||||
'Sending message with nonce $nonce',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final baseUrl = ref.read(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Access token is null');
|
||||
|
||||
final mockMessage = SnChatMessage(
|
||||
id: 'pending_$nonce',
|
||||
chatRoomId: _roomId,
|
||||
senderId: _identity.id,
|
||||
content: content,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
nonce: nonce,
|
||||
sender: _identity,
|
||||
);
|
||||
|
||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||
mockMessage,
|
||||
MessageStatus.pending,
|
||||
);
|
||||
|
||||
_pendingMessages[localMessage.id] = localMessage;
|
||||
_fileUploadProgress[localMessage.id] = {};
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||
|
||||
try {
|
||||
var cloudAttachments = List.empty(growable: true);
|
||||
for (var idx = 0; idx < attachments.length; idx++) {
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
fileData: attachments[idx],
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachments[idx].data.name ?? 'Post media',
|
||||
mimetype:
|
||||
attachments[idx].data.mimeType ??
|
||||
switch (attachments[idx].type) {
|
||||
UniversalFileType.image => 'image/unknown',
|
||||
UniversalFileType.video => 'video/unknown',
|
||||
UniversalFileType.audio => 'audio/unknown',
|
||||
UniversalFileType.file => 'application/octet-stream',
|
||||
},
|
||||
onProgress: (progress, _) {
|
||||
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
||||
onProgress?.call(
|
||||
localMessage.id,
|
||||
_fileUploadProgress[localMessage.id] ?? {},
|
||||
);
|
||||
},
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
cloudAttachments.add(cloudFile);
|
||||
}
|
||||
|
||||
final response = await _apiClient.request(
|
||||
editingTo == null
|
||||
? '/sphere/chat/$_roomId/messages'
|
||||
: '/sphere/chat/$_roomId/messages/${editingTo.id}',
|
||||
data: {
|
||||
'content': content,
|
||||
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
|
||||
'replied_message_id': replyingTo?.id,
|
||||
'forwarded_message_id': forwardingTo?.id,
|
||||
'meta': {},
|
||||
'nonce': nonce,
|
||||
},
|
||||
options: Options(method: editingTo == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
|
||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
_pendingMessages.remove(localMessage.id);
|
||||
await _database.deleteMessage(localMessage.id);
|
||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
if (editingTo != null) {
|
||||
final newMessages =
|
||||
currentMessages
|
||||
.where((m) => m.id != localMessage.id) // remove pending message
|
||||
.map(
|
||||
(m) => m.id == editingTo.id ? updatedMessage : m,
|
||||
) // update original message
|
||||
.toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
} else {
|
||||
final newMessages =
|
||||
currentMessages.map((m) {
|
||||
if (m.id == localMessage.id) {
|
||||
return updatedMessage;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
}
|
||||
developer.log(
|
||||
'Message with nonce $nonce sent successfully',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
'Failed to send message with nonce $nonce',
|
||||
name: 'MessagesNotifier',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
localMessage.status = MessageStatus.failed;
|
||||
_pendingMessages[localMessage.id] = localMessage;
|
||||
await _database.updateMessageStatus(
|
||||
localMessage.id,
|
||||
MessageStatus.failed,
|
||||
);
|
||||
final newMessages =
|
||||
(state.value ?? []).map((m) {
|
||||
if (m.id == localMessage.id) {
|
||||
return m..status = MessageStatus.failed;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> retryMessage(String pendingMessageId) async {
|
||||
developer.log(
|
||||
'Retrying message $pendingMessageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final message = await fetchMessageById(pendingMessageId);
|
||||
if (message == null) {
|
||||
throw Exception('Message not found');
|
||||
}
|
||||
|
||||
message.status = MessageStatus.pending;
|
||||
_pendingMessages[pendingMessageId] = message;
|
||||
await _database.updateMessageStatus(
|
||||
pendingMessageId,
|
||||
MessageStatus.pending,
|
||||
);
|
||||
|
||||
try {
|
||||
var remoteMessage = message.toRemoteMessage();
|
||||
final response = await _apiClient.post(
|
||||
'/sphere/chat/${message.roomId}/messages',
|
||||
data: {
|
||||
'content': remoteMessage.content,
|
||||
'attachments_id': remoteMessage.attachments,
|
||||
'meta': remoteMessage.meta,
|
||||
'nonce': message.nonce,
|
||||
},
|
||||
);
|
||||
|
||||
remoteMessage = SnChatMessage.fromJson(response.data);
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
_pendingMessages.remove(pendingMessageId);
|
||||
await _database.deleteMessage(pendingMessageId);
|
||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||
|
||||
final newMessages =
|
||||
(state.value ?? []).map((m) {
|
||||
if (m.id == pendingMessageId) {
|
||||
return updatedMessage;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
'Failed to retry message $pendingMessageId',
|
||||
name: 'MessagesNotifier',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
message.status = MessageStatus.failed;
|
||||
_pendingMessages[pendingMessageId] = message;
|
||||
await _database.updateMessageStatus(
|
||||
pendingMessageId,
|
||||
MessageStatus.failed,
|
||||
);
|
||||
final newMessages =
|
||||
(state.value ?? []).map((m) {
|
||||
if (m.id == pendingMessageId) {
|
||||
return m..status = MessageStatus.failed;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(_sortMessages(newMessages));
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
|
||||
if (remoteMessage.chatRoomId != _roomId) return;
|
||||
developer.log(
|
||||
'Received new message ${remoteMessage.id}',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
if (remoteMessage.nonce != null) {
|
||||
_pendingMessages.removeWhere(
|
||||
(_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce,
|
||||
);
|
||||
}
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final existingIndex = currentMessages.indexWhere(
|
||||
(m) =>
|
||||
m.id == localMessage.id ||
|
||||
(localMessage.nonce != null && m.nonce == localMessage.nonce),
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
final newList = [...currentMessages];
|
||||
newList[existingIndex] = localMessage;
|
||||
state = AsyncValue.data(_sortMessages(newList));
|
||||
} else {
|
||||
state = AsyncValue.data(
|
||||
_sortMessages([localMessage, ...currentMessages]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
||||
if (remoteMessage.chatRoomId != _roomId) return;
|
||||
developer.log(
|
||||
'Received message update ${remoteMessage.id}',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final index = currentMessages.indexWhere((m) => m.id == updatedMessage.id);
|
||||
|
||||
if (index >= 0) {
|
||||
final newList = [...currentMessages];
|
||||
newList[index] = updatedMessage;
|
||||
state = AsyncValue.data(_sortMessages(newList));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessageDeletion(String messageId) async {
|
||||
developer.log(
|
||||
'Received message deletion $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
_pendingMessages.remove(messageId);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final messageIndex = currentMessages.indexWhere((m) => m.id == messageId);
|
||||
|
||||
LocalChatMessage? messageToUpdate;
|
||||
if (messageIndex != -1) {
|
||||
messageToUpdate = currentMessages[messageIndex];
|
||||
} else {
|
||||
messageToUpdate = await fetchMessageById(messageId);
|
||||
}
|
||||
|
||||
if (messageToUpdate == null) return;
|
||||
|
||||
final remote = messageToUpdate.toRemoteMessage();
|
||||
final updatedRemote = remote.copyWith(
|
||||
content: 'This message was deleted',
|
||||
deletedAt: DateTime.now(),
|
||||
attachments: [],
|
||||
);
|
||||
|
||||
final deletedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
updatedRemote,
|
||||
messageToUpdate.status,
|
||||
);
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(deletedMessage));
|
||||
|
||||
if (messageIndex != -1) {
|
||||
final newList = [...currentMessages];
|
||||
newList[messageIndex] = deletedMessage;
|
||||
state = AsyncValue.data(newList);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMessage(String messageId) async {
|
||||
developer.log('Deleting message $messageId', name: 'MessagesNotifier');
|
||||
try {
|
||||
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
||||
await receiveMessageDeletion(messageId);
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
'Error deleting message $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
|
||||
_searchQuery = query.trim();
|
||||
_withLinks = withLinks;
|
||||
_withAttachments = withAttachments;
|
||||
loadInitial();
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
_searchQuery = null;
|
||||
_withLinks = null;
|
||||
_withAttachments = null;
|
||||
loadInitial();
|
||||
}
|
||||
|
||||
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
|
||||
developer.log(
|
||||
'Fetching message by id $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
try {
|
||||
final localMessage =
|
||||
await (_database.select(_database.chatMessages)
|
||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||
if (localMessage != null) {
|
||||
return _database.companionToMessage(localMessage);
|
||||
}
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/sphere/chat/$_roomId/messages/$messageId',
|
||||
);
|
||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
||||
final message = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
return message;
|
||||
} catch (e) {
|
||||
if (e is DioException) return null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> jumpToMessage(String messageId) async {
|
||||
developer.log(
|
||||
'Starting jump to message $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
if (_isJumping) {
|
||||
developer.log(
|
||||
'Jump already in progress, skipping',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
_isJumping = true;
|
||||
|
||||
try {
|
||||
developer.log('Fetching message $messageId', name: 'MessagesNotifier');
|
||||
final message = await fetchMessageById(messageId);
|
||||
if (message == null) {
|
||||
developer.log('Message $messageId not found', name: 'MessagesNotifier');
|
||||
showSnackBar('messageNotFound'.tr());
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if message is already in current state to avoid duplicate loading
|
||||
final currentMessages = state.value ?? [];
|
||||
final existingIndex = currentMessages.indexWhere(
|
||||
(m) => m.id == messageId,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
developer.log(
|
||||
'Message $messageId already in current state at index $existingIndex, jumping directly',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return existingIndex;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'Message $messageId not in current state, loading messages around it',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
// Count messages newer than this one
|
||||
final query = _database.customSelect(
|
||||
'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?',
|
||||
variables: [
|
||||
Variable.withString(_roomId),
|
||||
Variable.withDateTime(message.createdAt),
|
||||
],
|
||||
readsFrom: {_database.chatMessages},
|
||||
);
|
||||
final result = await query.getSingle();
|
||||
final newerCount = result.read<int>('count');
|
||||
|
||||
// Load messages around this position
|
||||
final offset =
|
||||
(newerCount - _pageSize ~/ 2).clamp(0, double.infinity).toInt();
|
||||
developer.log(
|
||||
'Loading messages with offset $offset, take $_pageSize',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final loadedMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: _pageSize,
|
||||
);
|
||||
|
||||
// Check if loaded messages are already in current state
|
||||
final currentIds = currentMessages.map((m) => m.id).toSet();
|
||||
final newMessages =
|
||||
loadedMessages.where((m) => !currentIds.contains(m.id)).toList();
|
||||
developer.log(
|
||||
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
if (newMessages.isNotEmpty) {
|
||||
// Merge with current messages
|
||||
final allMessages = [...currentMessages, ...newMessages];
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
final seenIds = <String>{};
|
||||
for (final message in allMessages) {
|
||||
if (seenIds.add(message.id)) {
|
||||
uniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
_sortMessages(uniqueMessages);
|
||||
state = AsyncValue.data(uniqueMessages);
|
||||
developer.log(
|
||||
'Updated state with ${uniqueMessages.length} total messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
}
|
||||
|
||||
final finalIndex = (state.value ?? []).indexWhere(
|
||||
(m) => m.id == messageId,
|
||||
);
|
||||
developer.log(
|
||||
'Final index for message $messageId is $finalIndex',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return finalIndex;
|
||||
} finally {
|
||||
_isJumping = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasLink(LocalChatMessage message) {
|
||||
final content = message.toRemoteMessage().content;
|
||||
if (content == null) return false;
|
||||
final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*');
|
||||
return urlRegex.hasMatch(content);
|
||||
}
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'room.dart';
|
||||
part of 'messages_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4';
|
||||
String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
@@ -30,7 +30,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
final user = SnAccount.fromJson(response.data);
|
||||
state = AsyncValue.data(user);
|
||||
|
||||
if (kIsWeb || !Platform.isLinux) {
|
||||
if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
|
||||
FirebaseAnalytics.instance.setUserId(id: user.id);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
@@ -44,9 +44,12 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
: 'failedToLoadUserInfoNetwork')
|
||||
.tr()
|
||||
.trim(),
|
||||
'${error.response!.statusCode}\n${error.response?.headers}',
|
||||
jsonEncode(error.response?.data),
|
||||
].join('\n\n'),
|
||||
'',
|
||||
'${error.response?.statusCode ?? 'Network Error'}',
|
||||
if (error.response?.headers != null) error.response?.headers,
|
||||
if (error.response?.data != null)
|
||||
jsonEncode(error.response?.data),
|
||||
].join('\n'),
|
||||
iconStyle: IconStyle.error,
|
||||
neutralButtonTitle: 'retry'.tr(),
|
||||
negativeButtonTitle: 'okay'.tr(),
|
||||
@@ -87,7 +90,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
final prefs = _ref.read(sharedPreferencesProvider);
|
||||
await prefs.remove(kTokenPairStoreKey);
|
||||
_ref.invalidate(tokenProvider);
|
||||
if (kIsWeb || !Platform.isLinux) {
|
||||
if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
|
||||
FirebaseAnalytics.instance.setUserId(id: null);
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/about.dart';
|
||||
import 'package:island/screens/account/credits.dart';
|
||||
import 'package:island/screens/developers/app_detail.dart';
|
||||
import 'package:island/screens/developers/bot_detail.dart';
|
||||
import 'package:island/screens/developers/edit_app.dart';
|
||||
@@ -19,6 +18,7 @@ import 'package:island/screens/developers/edit_project.dart';
|
||||
import 'package:island/screens/developers/new_project.dart';
|
||||
import 'package:island/screens/developers/project_detail.dart';
|
||||
import 'package:island/screens/discovery/articles.dart';
|
||||
import 'package:island/screens/files/file_list.dart';
|
||||
import 'package:island/screens/posts/post_categories_list.dart';
|
||||
import 'package:island/screens/posts/post_category_detail.dart';
|
||||
import 'package:island/screens/posts/post_search.dart';
|
||||
@@ -38,7 +38,7 @@ import 'package:island/screens/chat/chat.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
import 'package:island/screens/chat/room_detail.dart';
|
||||
import 'package:island/screens/chat/call.dart';
|
||||
import 'package:island/screens/chat/search_messages_screen.dart';
|
||||
import 'package:island/screens/chat/search_messages.dart';
|
||||
import 'package:island/screens/creators/hub.dart';
|
||||
import 'package:island/screens/creators/posts/post_manage_list.dart';
|
||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||
@@ -86,11 +86,7 @@ Widget _tabPagesTransitionBuilder(
|
||||
}
|
||||
|
||||
bool get _supportsAnalytics =>
|
||||
kIsWeb ||
|
||||
Platform.isAndroid ||
|
||||
Platform.isIOS ||
|
||||
Platform.isMacOS ||
|
||||
Platform.isWindows;
|
||||
kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
|
||||
|
||||
// Provider for the router
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
@@ -660,9 +656,9 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'socialCredits',
|
||||
path: '/account/credits',
|
||||
builder: (context, state) => const SocialCreditsScreen(),
|
||||
name: 'files',
|
||||
path: '/account/files',
|
||||
builder: (context, state) => const FileListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'relationships',
|
||||
|
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/services/udid.native.dart';
|
||||
import 'package:island/services/udid.dart' as udid;
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -68,7 +68,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
try {
|
||||
final deviceInfoPlugin = DeviceInfoPlugin();
|
||||
_deviceInfo = await deviceInfoPlugin.deviceInfo;
|
||||
_deviceUdid = await getUdid();
|
||||
_deviceUdid = await udid.getUdid();
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
@@ -174,12 +174,20 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
context,
|
||||
title: 'Device Information',
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.label,
|
||||
label: 'aboutDeviceName'.tr(),
|
||||
value:
|
||||
_deviceInfo?.data['name'] ?? 'unknown'.tr(),
|
||||
FutureBuilder<String>(
|
||||
future: udid.getDeviceName(),
|
||||
builder: (context, snapshot) {
|
||||
final value =
|
||||
snapshot.hasData
|
||||
? snapshot.data!
|
||||
: 'unknown'.tr();
|
||||
return _buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.label,
|
||||
label: 'aboutDeviceName'.tr(),
|
||||
value: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
|
@@ -141,20 +141,22 @@ class AccountScreen extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
GestureDetector(
|
||||
child: LevelingProgressCard(
|
||||
level: user.value!.profile.level,
|
||||
experience: user.value!.profile.experience,
|
||||
progress: user.value!.profile.levelingProgress,
|
||||
),
|
||||
LevelingProgressCard(
|
||||
isCompact: true,
|
||||
level: user.value!.profile.level,
|
||||
experience: user.value!.profile.experience,
|
||||
progress: user.value!.profile.levelingProgress,
|
||||
onTap: () {
|
||||
context.pushNamed('leveling');
|
||||
},
|
||||
).padding(horizontal: 12),
|
||||
const SizedBox.shrink(),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
@@ -181,6 +183,7 @@ class AccountScreen extends HookConsumerWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
@@ -206,7 +209,73 @@ class AccountScreen extends HookConsumerWidget {
|
||||
).height(140),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
).padding(horizontal: 12),
|
||||
const SizedBox.shrink(),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.settings, size: 28).padding(bottom: 8),
|
||||
Text('appSettings').tr().fontSize(16).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('settings');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.person_edit,
|
||||
size: 28,
|
||||
).padding(bottom: 8),
|
||||
Text('updateYourProfile').tr().fontSize(16).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('profileUpdate');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.manage_accounts,
|
||||
size: 28,
|
||||
).padding(bottom: 8),
|
||||
Text('accountSettings').tr().fontSize(16).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.notifications),
|
||||
@@ -235,6 +304,16 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('wallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.files),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('files').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('files');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.people),
|
||||
@@ -265,16 +344,6 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('webFeedMarketplace');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.star),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('credits').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('socialCredits');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('abuseReport').tr(),
|
||||
@@ -284,37 +353,6 @@ class AccountScreen extends HookConsumerWidget {
|
||||
onTap: () => context.pushNamed('reportList'),
|
||||
),
|
||||
const Divider(height: 1).padding(vertical: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.settings),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('appSettings').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('settings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.person_edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('updateYourProfile').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('profileUpdate');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.manage_accounts),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('accountSettings').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
const Divider(height: 1).padding(vertical: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.info),
|
||||
@@ -333,6 +371,8 @@ class AccountScreen extends HookConsumerWidget {
|
||||
title: Text('debugOptions').tr(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => DebugSheet(),
|
||||
);
|
||||
|
@@ -4,7 +4,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@@ -59,94 +58,93 @@ class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier
|
||||
}
|
||||
}
|
||||
|
||||
class SocialCreditsScreen extends HookConsumerWidget {
|
||||
const SocialCreditsScreen({super.key});
|
||||
class SocialCreditsTab extends HookConsumerWidget {
|
||||
const SocialCreditsTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final socialCredits = ref.watch(socialCreditsProvider);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text('socialCredits').tr()),
|
||||
body: Column(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
child: socialCredits
|
||||
.when(
|
||||
data:
|
||||
(credits) => Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
credits < 100
|
||||
? 'socialCreditsLevelPoor'.tr()
|
||||
: credits < 150
|
||||
? 'socialCreditsLevelNormal'.tr()
|
||||
: credits < 200
|
||||
? 'socialCreditsLevelGood'.tr()
|
||||
: 'socialCreditsLevelExcellent'.tr(),
|
||||
).tr().bold().fontSize(20),
|
||||
Text(
|
||||
'${credits.toStringAsFixed(2)} pts',
|
||||
).fontSize(14),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(value: credits / 200),
|
||||
],
|
||||
return Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
Card(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
child: socialCredits
|
||||
.when(
|
||||
data:
|
||||
(credits) => Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
credits < 100
|
||||
? 'socialCreditsLevelPoor'.tr()
|
||||
: credits < 150
|
||||
? 'socialCreditsLevelNormal'.tr()
|
||||
: credits < 200
|
||||
? 'socialCreditsLevelGood'.tr()
|
||||
: 'socialCreditsLevelExcellent'.tr(),
|
||||
).tr().bold().fontSize(20),
|
||||
Text(
|
||||
'${credits.toStringAsFixed(2)} pts',
|
||||
).fontSize(14),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(value: credits / 200),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Symbols.info),
|
||||
tooltip: 'socialCreditsDescription'.tr(),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Symbols.info),
|
||||
tooltip: 'socialCreditsDescription'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
error: (_, _) => Text('Error loading credits'),
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
)
|
||||
.padding(horizontal: 20, vertical: 16),
|
||||
),
|
||||
Expanded(
|
||||
child: PagingHelperView(
|
||||
provider: socialCreditHistoryNotifierProvider,
|
||||
futureRefreshable: socialCreditHistoryNotifierProvider.future,
|
||||
notifierRefreshable: socialCreditHistoryNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
final record = data.items[index];
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
error: (_, _) => Text('Error loading credits'),
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
)
|
||||
.padding(horizontal: 20, vertical: 16),
|
||||
),
|
||||
Expanded(
|
||||
child: PagingHelperView(
|
||||
provider: socialCreditHistoryNotifierProvider,
|
||||
futureRefreshable: socialCreditHistoryNotifierProvider.future,
|
||||
notifierRefreshable: socialCreditHistoryNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
final record = data.items[index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(record.reason),
|
||||
subtitle: Text(
|
||||
DateFormat.yMMMd().format(record.createdAt),
|
||||
title: Text(record.reason),
|
||||
subtitle: Text(
|
||||
DateFormat.yMMMd().format(record.createdAt),
|
||||
),
|
||||
trailing: Text(
|
||||
record.delta > 0
|
||||
? '+${record.delta}'
|
||||
: '${record.delta}',
|
||||
style: TextStyle(
|
||||
color: record.delta > 0 ? Colors.green : Colors.red,
|
||||
),
|
||||
trailing: Text(
|
||||
record.delta > 0
|
||||
? '+${record.delta}'
|
||||
: '${record.delta}',
|
||||
style: TextStyle(
|
||||
color: record.delta > 0 ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -4,12 +4,12 @@ import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/models/wallet.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/account/credits.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/widgets/account/leveling_progress.dart';
|
||||
@@ -89,7 +89,7 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
length: 3,
|
||||
child: AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('levelingProgress'.tr()),
|
||||
@@ -104,6 +104,15 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'socialCredits'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'stellarProgram'.tr(),
|
||||
@@ -119,6 +128,7 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_buildLevelingTab(context, ref, user.value!),
|
||||
const SocialCreditsTab(),
|
||||
_buildStellarProgramTab(context, ref),
|
||||
],
|
||||
),
|
||||
@@ -138,7 +148,6 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(20),
|
||||
@@ -164,10 +173,33 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
),
|
||||
const SliverGap(16),
|
||||
|
||||
// Stairs visualization with fixed height and horizontal scroll
|
||||
SliverToBoxAdapter(child: _buildLevelStairs(context, currentLevel)),
|
||||
const SliverGap(24),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'${'levelingProgressLevel'.tr(args: [currentLevel.toString()])} / 120',
|
||||
textAlign: TextAlign.start,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(
|
||||
value: currentLevel / 120,
|
||||
minHeight: 10,
|
||||
stopIndicatorRadius: 0,
|
||||
trackGap: 0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, top: 16, bottom: 12),
|
||||
),
|
||||
),
|
||||
const SliverGap(16),
|
||||
// Leveling History
|
||||
SliverToBoxAdapter(
|
||||
child: Text(
|
||||
@@ -239,137 +271,12 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMembershipSection(context, ref, stellarSubscription),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevelStairs(BuildContext context, int currentLevel) {
|
||||
const totalLevels = 14;
|
||||
const stairHeight = 20.0;
|
||||
const stairWidth = 50.0;
|
||||
const containerHeight = 280.0;
|
||||
|
||||
return Container(
|
||||
height: containerHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SizedBox(
|
||||
width: (totalLevels * (stairWidth + 8)) + 40,
|
||||
height: containerHeight,
|
||||
child: CustomPaint(
|
||||
painter: LevelStairsPainter(
|
||||
currentLevel: currentLevel,
|
||||
totalLevels: totalLevels,
|
||||
primaryColor: Theme.of(context).colorScheme.primary,
|
||||
surfaceColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
onSurfaceColor: Theme.of(context).colorScheme.onSurface,
|
||||
stairHeight: stairHeight,
|
||||
stairWidth: stairWidth,
|
||||
),
|
||||
child: Stack(
|
||||
children: List.generate(totalLevels, (index) {
|
||||
final level = index + 1;
|
||||
final isCompleted = level <= currentLevel;
|
||||
final isCurrent = level == currentLevel;
|
||||
|
||||
// Calculate position from bottom
|
||||
final bottomPosition = 0.0;
|
||||
final leftPosition = 20.0 + (index * (stairWidth + 8));
|
||||
|
||||
// Make higher levels progressively taller
|
||||
final progressiveHeight =
|
||||
40.0 + (index * 15.0); // Base height + progressive increase
|
||||
|
||||
return Positioned(
|
||||
left: leftPosition,
|
||||
bottom: bottomPosition,
|
||||
child: Container(
|
||||
width: stairWidth,
|
||||
height: progressiveHeight,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isCompleted
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(6),
|
||||
topRight: Radius.circular(6),
|
||||
),
|
||||
border:
|
||||
isCurrent
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
boxShadow:
|
||||
isCurrent
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.3),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
level.toString(),
|
||||
style: GoogleFonts.robotoMono(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
isCompleted
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (isCurrent) ...[
|
||||
const Gap(4),
|
||||
Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMembershipSection(context, ref, stellarSubscription),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -66,7 +66,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -32,7 +32,7 @@ import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:island/widgets/safety/abuse_report_helper.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:island/services/color_extraction.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@@ -581,14 +581,14 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
|
||||
try {
|
||||
final account = await ref.watch(accountProvider(uname).future);
|
||||
if (account.profile.background == null) return null;
|
||||
final palette = await PaletteGenerator.fromImageProvider(
|
||||
final colors = await ColorExtractionService.getColorsFromImage(
|
||||
CloudImageWidget.provider(
|
||||
fileId: account.profile.background!.id,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
),
|
||||
);
|
||||
final dominantColor = palette.dominantColor?.color;
|
||||
if (dominantColor == null) return null;
|
||||
if (colors.isEmpty) return null;
|
||||
final dominantColor = colors.first;
|
||||
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
@@ -268,7 +268,7 @@ class _AccountBadgesProviderElement
|
||||
}
|
||||
|
||||
String _$accountAppbarForcegroundColorHash() =>
|
||||
r'8ee0cae10817b77fb09548a482f5247662b4374c';
|
||||
r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0';
|
||||
|
||||
/// See also [accountAppbarForcegroundColor].
|
||||
@ProviderFor(accountAppbarForcegroundColor)
|
||||
|
@@ -1,17 +1,10 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'captcha.config.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<String> captchaUrl(Ref ref) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/.well-known/services');
|
||||
final serviceMapping = await resp.data;
|
||||
var baseUrl = serviceMapping['DysonNetwork.Pass'] as String;
|
||||
// The backend using self-signed certicates on development
|
||||
// Which mobile simulator might not accept, use this to avoid errors
|
||||
if (baseUrl.contains('https://localhost')) baseUrl = 'http://localhost:5216';
|
||||
return '$baseUrl/captcha';
|
||||
const baseUrl = "https://solian.app";
|
||||
return '$baseUrl/auth/captcha';
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ part of 'captcha.config.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$captchaUrlHash() => r'bbed0d18272dd205069642b3c6583ea2eef735d1';
|
||||
String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8';
|
||||
|
||||
/// See also [captchaUrl].
|
||||
@ProviderFor(captchaUrl)
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -42,22 +39,6 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||
4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
|
||||
};
|
||||
|
||||
Future<String?> getDeviceName() async {
|
||||
if (kIsWeb) return null;
|
||||
String? name;
|
||||
if (Platform.isIOS) {
|
||||
final deviceInfo = await DeviceInfoPlugin().iosInfo;
|
||||
name = deviceInfo.name;
|
||||
} else if (Platform.isAndroid) {
|
||||
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||
name = deviceInfo.name;
|
||||
} else if (Platform.isWindows) {
|
||||
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
|
||||
name = deviceInfo.computerName;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
class LoginScreen extends HookConsumerWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
|
@@ -543,7 +543,7 @@ class EditChatScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
221
lib/screens/chat/public_room_preview.dart
Normal file
221
lib/screens/chat/public_room_preview.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:gap/gap.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/widgets/content/cloud_files.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:go_router/go_router.dart";
|
||||
import "package:material_symbols_icons/symbols.dart";
|
||||
import "package:styled_widget/styled_widget.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
import "package:island/widgets/app_scaffold.dart";
|
||||
import "package:island/widgets/chat/message_item.dart";
|
||||
import "package:island/widgets/response.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
|
||||
class PublicRoomPreview extends HookConsumerWidget {
|
||||
final String id;
|
||||
final SnChatRoom room;
|
||||
|
||||
const PublicRoomPreview({super.key, required this.id, required this.room});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final messages = ref.watch(messagesNotifierProvider(id));
|
||||
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
|
||||
var isLoading = false;
|
||||
|
||||
// Add scroll listener for pagination
|
||||
useEffect(() {
|
||||
void onScroll() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
messagesNotifier.loadMore().then((_) => isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
scrollController.addListener(onScroll);
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||
SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
final valueKey = key as ValueKey;
|
||||
final messageId = valueKey.value as String;
|
||||
return messageList.indexWhere((m) => m.id == messageId);
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
|
||||
return MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false, // User is not a member, so not current user
|
||||
onAction: null, // No actions allowed in preview mode
|
||||
onJump: (_) {}, // No jump functionality in preview
|
||||
progress: null,
|
||||
showAvatar: isLastInGroup,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final compactHeader = isWideScreen(context);
|
||||
|
||||
Widget comfortHeaderWidget() => Column(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child:
|
||||
(room.type == 1 && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
child: Text(
|
||||
room.name![0].toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(room.type == 1 && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(15),
|
||||
],
|
||||
);
|
||||
|
||||
Widget compactHeaderWidget() => Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child:
|
||||
(room.type == 1 && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
child: Text(
|
||||
room.name![0].toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(room.type == 1 && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(19),
|
||||
],
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
||||
automaticallyImplyLeading: false,
|
||||
toolbarHeight: compactHeader ? null : 64,
|
||||
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
context.pushNamed('chatDetail', pathParameters: {'id': id});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: messages.when(
|
||||
data:
|
||||
(messageList) =>
|
||||
messageList.isEmpty
|
||||
? Center(child: Text('No messages yet'.tr()))
|
||||
: chatMessageListWidget(messageList),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => messagesNotifier.loadInitial(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Join button at the bottom for public rooms
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post('/sphere/chat/${room.id}/members/me');
|
||||
ref.invalidate(chatroomIdentityProvider(id));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
},
|
||||
label: Text('chatJoin').tr(),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/pods/database.dart';
|
||||
import 'package:island/screens/chat/search_messages.dart';
|
||||
|
||||
part 'room_detail.freezed.dart';
|
||||
part 'room_detail.g.dart';
|
||||
@@ -401,11 +402,17 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
onTap: () async {
|
||||
final result = await context.pushNamed(
|
||||
'searchMessages',
|
||||
pathParameters: {'id': id},
|
||||
);
|
||||
if (result is SearchMessagesResult) {
|
||||
// Navigate back to room screen with message to jump to
|
||||
if (context.mounted) {
|
||||
context.pop(result.messageId);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@@ -1,13 +1,20 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
import 'package:island/pods/messages_notifier.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/message_item.dart';
|
||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
// Class to represent the result when popping from search messages
|
||||
class SearchMessagesResult {
|
||||
final String messageId;
|
||||
const SearchMessagesResult(this.messageId);
|
||||
}
|
||||
|
||||
class SearchMessagesScreen extends HookConsumerWidget {
|
||||
final String roomId;
|
||||
|
||||
@@ -112,24 +119,24 @@ class SearchMessagesScreen extends HookConsumerWidget {
|
||||
? Center(child: Text('noMessagesFound'.tr()))
|
||||
: SuperListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
reverse: false, // Show newest messages at the top
|
||||
itemCount: messageList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
// Simplified MessageItem for search results, no grouping logic
|
||||
return MessageItem(
|
||||
return MessageListTile(
|
||||
message: message,
|
||||
isCurrentUser:
|
||||
false, // Or determine based on actual user
|
||||
onAction: null,
|
||||
onJump: (_) {},
|
||||
progress: null,
|
||||
showAvatar: true,
|
||||
onJump: (messageId) {
|
||||
// Return the search result and pop back to room detail
|
||||
context.pop(SearchMessagesResult(messageId));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
|
||||
error:
|
||||
(error, _) => Center(
|
||||
child: Text('errorGeneric'.tr(args: [error.toString()])),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
@@ -78,6 +78,7 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
replacePath: true,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
CropAspectRatio(height: 7, width: 16)
|
||||
@@ -98,7 +99,7 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -127,7 +127,7 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
556
lib/screens/files/file_list.dart
Normal file
556
lib/screens/files/file_list.dart
Normal file
@@ -0,0 +1,556 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/file_pool.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'file_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<SnCloudFile> {
|
||||
String? _poolId;
|
||||
bool _includeRecycled = false;
|
||||
|
||||
void setFilters(String? poolId, bool includeRecycled) {
|
||||
_poolId = poolId;
|
||||
_includeRecycled = includeRecycled;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
final take = 20;
|
||||
|
||||
final queryParameters = <String, dynamic>{'offset': offset, 'take': take};
|
||||
|
||||
// Add filter parameters
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
if (_includeRecycled) {
|
||||
queryParameters['recycled'] = 'true';
|
||||
}
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/files/me',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<SnCloudFile> items =
|
||||
(response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
|
||||
final hasMore = offset + items.length < total;
|
||||
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||
|
||||
return CursorPagingData(
|
||||
items: items,
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/usage');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/quota');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
class FileListScreen extends HookConsumerWidget {
|
||||
const FileListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Filter state
|
||||
final selectedPool = useState<String?>(null);
|
||||
final includeRecycled = useState(false);
|
||||
|
||||
final usageAsync = ref.watch(billingUsageProvider);
|
||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||
|
||||
// Update notifier filters when state changes
|
||||
useEffect(() {
|
||||
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||
notifier.setFilters(selectedPool.value, includeRecycled.value);
|
||||
return null;
|
||||
}, [selectedPool.value, includeRecycled.value]);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text('Files'), leading: const PageBackButton()),
|
||||
body: usageAsync.when(
|
||||
data:
|
||||
(usage) => quotaAsync.when(
|
||||
data:
|
||||
(quota) => _buildQuotaUI(
|
||||
usage,
|
||||
quota,
|
||||
ref,
|
||||
selectedPool,
|
||||
includeRecycled,
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error loading quota')),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error loading usage')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuotaUI(
|
||||
Map<String, dynamic>? usage,
|
||||
Map<String, dynamic>? quota,
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedPool,
|
||||
ValueNotifier<bool> includeRecycled,
|
||||
) {
|
||||
if (usage == null) return const SizedBox.shrink();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Uploads',
|
||||
'${((usage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Files',
|
||||
'${usage['total_file_count']}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Quota',
|
||||
'${usage['total_quota']} MiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Used Quota',
|
||||
'${((usage['used_quota'] as num) / (usage['total_quota'] as num) * 100).toStringAsFixed(2)}%',
|
||||
progress:
|
||||
(usage['used_quota'] as num) /
|
||||
(usage['total_quota'] as num),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Pool Usage'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildPoolChartData(usage)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Verbose Quota'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildQuotaChartData(quota)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildFilters(ref, selectedPool, includeRecycled),
|
||||
),
|
||||
const SliverGap(8),
|
||||
PagingHelperSliverView(
|
||||
provider: cloudFileListNotifierProvider,
|
||||
futureRefreshable: cloudFileListNotifierProvider.future,
|
||||
notifierRefreshable: cloudFileListNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
final itemType = item.mimeType?.split('/').firstOrNull;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: switch (itemType) {
|
||||
'image' => CloudImageWidget(file: item),
|
||||
'audio' =>
|
||||
const Icon(Symbols.audio_file, fill: 1).center(),
|
||||
'video' =>
|
||||
const Icon(Symbols.video_file, fill: 1).center(),
|
||||
_ =>
|
||||
const Icon(Symbols.body_system, fill: 1).center(),
|
||||
},
|
||||
),
|
||||
),
|
||||
title:
|
||||
item.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(formatFileSize(item.size)),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) showLoadingModal(context);
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/${item.id}');
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildPoolChartData(Map<String, dynamic> usage) {
|
||||
final pools = usage['pool_usages'] as List<dynamic>;
|
||||
final colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.red,
|
||||
Colors.purple,
|
||||
];
|
||||
return PieChartData(
|
||||
sections:
|
||||
pools.asMap().entries.map((entry) {
|
||||
final pool = entry.value as Map<String, dynamic>;
|
||||
final title = pool['pool_name'] as String;
|
||||
final truncatedTitle =
|
||||
title.length > 8 ? '${title.substring(0, 8)}...' : title;
|
||||
return PieChartSectionData(
|
||||
value: (pool['usage_bytes'] as num).toDouble(),
|
||||
title: truncatedTitle,
|
||||
color: colors[entry.key % colors.length],
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) {
|
||||
if (quota == null) return PieChartData(sections: []);
|
||||
return PieChartData(
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: (quota['based_quota'] as num).toDouble(),
|
||||
title: 'Base',
|
||||
color: Colors.green,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: (quota['extra_quota'] as num).toDouble(),
|
||||
title: 'Extra',
|
||||
color: Colors.orange,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters(
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedPool,
|
||||
ValueNotifier<bool> includeRecycled,
|
||||
) {
|
||||
final poolsAsync = ref.watch(poolsProvider);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'filters'.tr(),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 600;
|
||||
return isWide
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: poolsAsync.when(
|
||||
data:
|
||||
(pools) => DropdownButtonFormField<String?>(
|
||||
value: selectedPool.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Pool',
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('allPools'.tr()),
|
||||
),
|
||||
...pools.map(
|
||||
(pool) => DropdownMenuItem<String?>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
(value) => selectedPool.value = value,
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, _) => const Text('Error loading pools'),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text('includeRecycled'.tr()),
|
||||
const Gap(8),
|
||||
Switch(
|
||||
value: includeRecycled.value,
|
||||
onChanged:
|
||||
(value) => includeRecycled.value = value,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
tooltip: 'deleteRecycledFiles'.tr(),
|
||||
onPressed:
|
||||
includeRecycled.value
|
||||
? () => _deleteRecycledFiles(ref)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
poolsAsync.when(
|
||||
data:
|
||||
(pools) => DropdownButtonFormField<String?>(
|
||||
value: selectedPool.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pool',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('allPools'.tr()),
|
||||
),
|
||||
...pools.map(
|
||||
(pool) => DropdownMenuItem<String?>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
(value) => selectedPool.value = value,
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, _) => const Text('Error loading pools'),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
children: [
|
||||
Text('includeRecycled'.tr()),
|
||||
const Gap(8),
|
||||
Switch(
|
||||
value: includeRecycled.value,
|
||||
onChanged:
|
||||
(value) => includeRecycled.value = value,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
tooltip: 'deleteRecycledFiles'.tr(),
|
||||
onPressed:
|
||||
includeRecycled.value
|
||||
? () => _deleteRecycledFiles(ref)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 8);
|
||||
}
|
||||
|
||||
Future<void> _deleteRecycledFiles(WidgetRef ref) async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteRecycledFiles'.tr(),
|
||||
'deleteRecycledFiles'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (ref.context.mounted) showLoadingModal(ref.context);
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/recycled');
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
showSnackBar('recycledFilesDeleted'.tr());
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteRecycledFiles'.tr());
|
||||
} finally {
|
||||
if (ref.context.mounted) hideLoadingModal(ref.context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStatCard(String label, String value, {double? progress}) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (progress != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(value: progress),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
69
lib/screens/files/file_list.g.dart
Normal file
69
lib/screens/files/file_list.g.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_list.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$billingUsageHash() => r'58d8bc774868d60781574c85d6b25869a79c57aa';
|
||||
|
||||
/// See also [billingUsage].
|
||||
@ProviderFor(billingUsage)
|
||||
final billingUsageProvider =
|
||||
AutoDisposeFutureProvider<Map<String, dynamic>?>.internal(
|
||||
billingUsage,
|
||||
name: r'billingUsageProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$billingUsageHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef BillingUsageRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$billingQuotaHash() => r'4ec5d728e439015800abb2d0d673b5a7329cc654';
|
||||
|
||||
/// See also [billingQuota].
|
||||
@ProviderFor(billingQuota)
|
||||
final billingQuotaProvider =
|
||||
AutoDisposeFutureProvider<Map<String, dynamic>?>.internal(
|
||||
billingQuota,
|
||||
name: r'billingQuotaProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$billingQuotaHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$cloudFileListNotifierHash() =>
|
||||
r'22c45a8ea23147a3835ba870ad2f0bb833f853ea';
|
||||
|
||||
/// See also [CloudFileListNotifier].
|
||||
@ProviderFor(CloudFileListNotifier)
|
||||
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
CloudFileListNotifier,
|
||||
CursorPagingData<SnCloudFile>
|
||||
>.internal(
|
||||
CloudFileListNotifier.new,
|
||||
name: r'cloudFileListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$cloudFileListNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$CloudFileListNotifier =
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@@ -39,7 +39,7 @@ class NotificationUnreadCountNotifier
|
||||
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/pusher/notifications/count');
|
||||
final response = await client.get('/ring/notifications/count');
|
||||
return (response.data as num).toInt();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
@@ -89,7 +89,7 @@ class NotificationListNotifier extends _$NotificationListNotifier
|
||||
final queryParams = {'offset': offset, 'take': _pageSize};
|
||||
|
||||
final response = await client.get(
|
||||
'/pusher/notifications',
|
||||
'/ring/notifications',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
@@ -121,7 +121,7 @@ class NotificationScreen extends HookConsumerWidget {
|
||||
Future<void> markAllRead() async {
|
||||
showLoadingModal(context);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
await apiClient.post('/pusher/notifications/all/read');
|
||||
await apiClient.post('/ring/notifications/all/read');
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
ref.invalidate(notificationListNotifierProvider);
|
||||
|
@@ -7,7 +7,7 @@ part of 'notification.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$notificationUnreadCountNotifierHash() =>
|
||||
r'0763b66bd64e5a9b7c317887e109ab367515dfa4';
|
||||
r'08c773809958d96a7ce82acf04af1f9e0b23e119';
|
||||
|
||||
/// See also [NotificationUnreadCountNotifier].
|
||||
@ProviderFor(NotificationUnreadCountNotifier)
|
||||
@@ -28,7 +28,7 @@ final notificationUnreadCountNotifierProvider =
|
||||
|
||||
typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
|
||||
String _$notificationListNotifierHash() =>
|
||||
r'5099466db475bbcf1ab6b514eb072f1dc4c6f930';
|
||||
r'260046e11f45b0d67ab25bcbdc8604890d71ccc7';
|
||||
|
||||
/// See also [NotificationListNotifier].
|
||||
@ProviderFor(NotificationListNotifier)
|
||||
|
@@ -10,6 +10,7 @@ import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/screens/posts/compose_article.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/attachment_uploader.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
@@ -225,8 +226,26 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
return AttachmentPreview(
|
||||
item: state.attachments.value[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload:
|
||||
() => ComposeLogic.uploadAttachment(ref, state, idx),
|
||||
onRequestUpload: () async {
|
||||
final config = await showModalBottomSheet<AttachmentUploadConfig>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||
onUpdate:
|
||||
(value) => ComposeLogic.updateAttachment(state, value, idx),
|
||||
@@ -253,8 +272,27 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
return AttachmentPreview(
|
||||
item: state.attachments.value[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload:
|
||||
() => ComposeLogic.uploadAttachment(ref, state, idx),
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<AttachmentUploadConfig>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onDelete:
|
||||
() => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||
onUpdate:
|
||||
|
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -12,6 +11,7 @@ import 'package:island/models/post.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/attachment_uploader.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
@@ -346,12 +346,30 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload:
|
||||
() => ComposeLogic.uploadAttachment(
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<
|
||||
AttachmentUploadConfig
|
||||
>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) =>
|
||||
AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
),
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onUpdate:
|
||||
(value) =>
|
||||
ComposeLogic.updateAttachment(
|
||||
|
@@ -21,7 +21,7 @@ import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:island/services/color_extraction.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -278,14 +278,14 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
|
||||
try {
|
||||
final publisher = await ref.watch(publisherProvider(pubName).future);
|
||||
if (publisher.background == null) return null;
|
||||
final palette = await PaletteGenerator.fromImageProvider(
|
||||
final colors = await ColorExtractionService.getColorsFromImage(
|
||||
CloudImageWidget.provider(
|
||||
fileId: publisher.background!.id,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
),
|
||||
);
|
||||
final dominantColor = palette.dominantColor?.color;
|
||||
if (dominantColor == null) return null;
|
||||
if (colors.isEmpty) return null;
|
||||
final dominantColor = colors.first;
|
||||
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
@@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement
|
||||
}
|
||||
|
||||
String _$publisherAppbarForcegroundColorHash() =>
|
||||
r'd781a806a242aea5c1609ec98c97c52fdd9f7db1';
|
||||
r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7';
|
||||
|
||||
/// See also [publisherAppbarForcegroundColor].
|
||||
@ProviderFor(publisherAppbarForcegroundColor)
|
||||
|
@@ -8,7 +8,7 @@ import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/account/account_pfc.dart';
|
||||
import 'package:island/widgets/account/status.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:island/services/color_extraction.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -32,14 +32,14 @@ part 'realm_detail.g.dart';
|
||||
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
|
||||
final realm = await ref.watch(realmProvider(realmSlug).future);
|
||||
if (realm?.background == null) return null;
|
||||
final palette = await PaletteGenerator.fromImageProvider(
|
||||
final colors = await ColorExtractionService.getColorsFromImage(
|
||||
CloudImageWidget.provider(
|
||||
fileId: realm!.background!.id,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
),
|
||||
);
|
||||
final dominantColor = palette.dominantColor?.color;
|
||||
if (dominantColor == null) return null;
|
||||
if (colors.isEmpty) return null;
|
||||
final dominantColor = colors.first;
|
||||
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,7 @@ part of 'realm_detail.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$realmAppbarForegroundColorHash() =>
|
||||
r'14b5563d861996ea182d0d2db7aa5c2bb3bbaf48';
|
||||
r'8131c047a984318a4cc3fbb5daa5ef0ce44dfae5';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -211,7 +211,7 @@ class EditRealmScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Access token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -12,14 +12,16 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/services/color_extraction.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/file_pool.dart';
|
||||
|
||||
class SettingsScreen extends HookConsumerWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -33,7 +35,8 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
final isDesktop =
|
||||
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
final pools = ref.watch(poolsProvider);
|
||||
final user = ref.watch(userInfoProvider);
|
||||
final docBasepath = useState<String?>(null);
|
||||
|
||||
useEffect(() {
|
||||
@@ -127,6 +130,48 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Message display style settings
|
||||
ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsMessageDisplayStyle').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.chat),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'bubble',
|
||||
child: Text('Bubble').fontSize(14),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'column',
|
||||
child: Text('Column').fontSize(14),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'compact',
|
||||
child: Text('Compact').fontSize(14),
|
||||
),
|
||||
],
|
||||
value: settings.messageDisplayStyle,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setMessageDisplayStyle(value);
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
height: 40,
|
||||
width: 140,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(height: 40),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Color scheme settings
|
||||
ListTile(
|
||||
minLeadingWidth: 48,
|
||||
@@ -293,24 +338,26 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () async {
|
||||
showLoadingModal(context);
|
||||
final palette = await PaletteGenerator.fromImageProvider(
|
||||
final colors = await ColorExtractionService.getColorsFromImage(
|
||||
FileImage(
|
||||
File('${docBasepath.value}/$kAppBackgroundImagePath'),
|
||||
),
|
||||
);
|
||||
if (palette.darkVibrantColor == null ||
|
||||
palette.lightVibrantColor == null) {
|
||||
if (colors.isEmpty) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
showErrorAlert(
|
||||
'Unable to calculate the domiant color of the background image.',
|
||||
'Unable to calculate the dominant color of the background image.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: colors.first,
|
||||
);
|
||||
final color =
|
||||
MediaQuery.of(context).platformBrightness == Brightness.dark
|
||||
? palette.darkVibrantColor!.color
|
||||
: palette.lightVibrantColor!.color;
|
||||
? colorScheme.primary
|
||||
: colorScheme.primary;
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setAppColorScheme(color.value);
|
||||
@@ -365,6 +412,71 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (user.value != null)
|
||||
pools.when(
|
||||
data: (data) {
|
||||
final validPools = data;
|
||||
final currentPoolId = resolveDefaultPoolId(ref, data);
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsDefaultPool').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.cloud),
|
||||
subtitle: Text(
|
||||
'settingsDefaultPoolHelper'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
isExpanded: true,
|
||||
items:
|
||||
validPools.map((p) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: p.id,
|
||||
child: Tooltip(
|
||||
message: p.name,
|
||||
child: Text(
|
||||
p.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).fontSize(14),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
value: currentPoolId,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setDefaultPoolId(value);
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
height: 40,
|
||||
width: 120,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(height: 40),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading:
|
||||
() => const ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('Loading pools...'),
|
||||
leading: CircularProgressIndicator(),
|
||||
),
|
||||
error:
|
||||
(err, st) => ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsDefaultPool').tr(),
|
||||
subtitle: Text('Error: $err'),
|
||||
leading: const Icon(Icons.error, color: Colors.red),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final behaviorSettings = [
|
||||
@@ -467,8 +579,33 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
];
|
||||
|
||||
// Desktop-specific settings
|
||||
// But nothing for now
|
||||
final desktopSettings = !isDesktop ? <Widget>[] : <Widget>[];
|
||||
final desktopSettings =
|
||||
!isDesktop
|
||||
? <Widget>[]
|
||||
: [
|
||||
ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsWindowOpacity').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.opacity),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Slider(
|
||||
value: settings.windowOpacity,
|
||||
min: 0.1,
|
||||
max: 1.0,
|
||||
year2023: true,
|
||||
padding: EdgeInsets.only(right: 24),
|
||||
label: '${(settings.windowOpacity * 100).round()}%',
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setWindowOpacity(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// Create a responsive layout based on screen width
|
||||
Widget buildSettingsList() {
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class TrayService {
|
||||
TrayService._();
|
||||
@@ -48,14 +47,10 @@ class TrayService {
|
||||
void handleAction(MenuItem item) {
|
||||
switch (item.key) {
|
||||
case 'show_window':
|
||||
if (appWindow.isVisible) {
|
||||
appWindow.restore();
|
||||
} else {
|
||||
appWindow.show();
|
||||
}
|
||||
windowManager.show();
|
||||
break;
|
||||
case 'exit_app':
|
||||
appWindow.close();
|
||||
windowManager.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
49
lib/services/color_extraction.dart
Normal file
49
lib/services/color_extraction.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:material_color_utilities/material_color_utilities.dart' as mcu;
|
||||
|
||||
class ColorExtractionService {
|
||||
/// Extracts dominant colors from an image provider.
|
||||
/// Returns a list of colors suitable for UI theming.
|
||||
static Future<List<Color>> getColorsFromImage(ImageProvider provider) async {
|
||||
try {
|
||||
if (provider is FileImage) {
|
||||
final bytes = await provider.file.readAsBytes();
|
||||
final image = img.decodeImage(bytes);
|
||||
if (image == null) return [];
|
||||
final Map<int, int> colorToCount = {};
|
||||
for (int y = 0; y < image.height; y++) {
|
||||
for (int x = 0; x < image.width; x++) {
|
||||
final pixel = image.getPixel(x, y) as int;
|
||||
final r = (pixel >> 24) & 0xff;
|
||||
final g = (pixel >> 16) & 0xff;
|
||||
final b = (pixel >> 8) & 0xff;
|
||||
final a = pixel & 0xff;
|
||||
if (a == 0) continue;
|
||||
final argb = (a << 24) | (r << 16) | (g << 8) | b;
|
||||
colorToCount[argb] = (colorToCount[argb] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
final List<int> filteredResults = mcu.Score.score(
|
||||
colorToCount,
|
||||
desired: 1,
|
||||
filter: true,
|
||||
);
|
||||
final List<int> scoredResults = mcu.Score.score(
|
||||
colorToCount,
|
||||
desired: 4,
|
||||
filter: false,
|
||||
);
|
||||
return <dynamic>{
|
||||
...filteredResults,
|
||||
...scoredResults,
|
||||
}.toList().map((argb) => Color(argb)).toList();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error getting colors from image: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,21 +1,23 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:croppy/croppy.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:native_exif/native_exif.dart';
|
||||
import 'package:tus_client_dart/tus_client_dart.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
enum FileUploadMode { generic, mediaSafe }
|
||||
|
||||
Future<XFile?> cropImage(
|
||||
BuildContext context, {
|
||||
required XFile image,
|
||||
List<CropAspectRatio?>? allowedAspectRatios,
|
||||
bool replacePath = false,
|
||||
bool replacePath = true,
|
||||
}) async {
|
||||
final result = await showMaterialImageCropper(
|
||||
context,
|
||||
@@ -40,64 +42,63 @@ Future<XFile?> cropImage(
|
||||
);
|
||||
}
|
||||
|
||||
Completer<SnCloudFile?> putMediaToCloud({
|
||||
Completer<SnCloudFile?> putFileToCloud({
|
||||
required UniversalFile fileData,
|
||||
required String atk,
|
||||
required String baseUrl,
|
||||
String? poolId,
|
||||
String? filename,
|
||||
String? mimetype,
|
||||
FileUploadMode? mode,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
}) {
|
||||
final completer = Completer<SnCloudFile?>();
|
||||
|
||||
// Process the image to remove GPS EXIF data if needed
|
||||
if (fileData.isOnDevice && fileData.type == UniversalFileType.image) {
|
||||
final effectiveMode =
|
||||
mode ??
|
||||
(fileData.type == UniversalFileType.file
|
||||
? FileUploadMode.generic
|
||||
: FileUploadMode.mediaSafe);
|
||||
|
||||
if (effectiveMode == FileUploadMode.mediaSafe &&
|
||||
fileData.isOnDevice &&
|
||||
fileData.type == UniversalFileType.image) {
|
||||
final data = fileData.data;
|
||||
if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||
// Use native_exif to selectively remove GPS data
|
||||
Exif.fromPath(data.path)
|
||||
.then((exif) {
|
||||
// Remove GPS-related attributes
|
||||
final gpsAttributes = [
|
||||
'GPSLatitude',
|
||||
'GPSLatitudeRef',
|
||||
'GPSLongitude',
|
||||
'GPSLongitudeRef',
|
||||
'GPSAltitude',
|
||||
'GPSAltitudeRef',
|
||||
'GPSTimeStamp',
|
||||
'GPSProcessingMethod',
|
||||
'GPSDateStamp',
|
||||
];
|
||||
|
||||
// Create a map of attributes to clear
|
||||
final clearAttributes = <String, String>{};
|
||||
for (final attr in gpsAttributes) {
|
||||
clearAttributes[attr] = '';
|
||||
}
|
||||
|
||||
// Write empty values to remove GPS data
|
||||
return exif.writeAttributes(clearAttributes);
|
||||
.then((exif) async {
|
||||
final gpsAttributes = {
|
||||
'GPSLatitude': '',
|
||||
'GPSLatitudeRef': '',
|
||||
'GPSLongitude': '',
|
||||
'GPSLongitudeRef': '',
|
||||
'GPSAltitude': '',
|
||||
'GPSAltitudeRef': '',
|
||||
'GPSTimeStamp': '',
|
||||
'GPSProcessingMethod': '',
|
||||
'GPSDateStamp': '',
|
||||
};
|
||||
await exif.writeAttributes(gpsAttributes);
|
||||
})
|
||||
.then((_) {
|
||||
// Continue with upload after GPS data is removed
|
||||
_processUpload(
|
||||
.then(
|
||||
(_) => _processUpload(
|
||||
fileData,
|
||||
atk,
|
||||
baseUrl,
|
||||
poolId,
|
||||
filename,
|
||||
mimetype,
|
||||
onProgress,
|
||||
completer,
|
||||
);
|
||||
})
|
||||
),
|
||||
)
|
||||
.catchError((e) {
|
||||
// If there's an error, continue with the original file
|
||||
debugPrint('Error removing GPS EXIF data: $e');
|
||||
_processUpload(
|
||||
return _processUpload(
|
||||
fileData,
|
||||
atk,
|
||||
baseUrl,
|
||||
poolId,
|
||||
filename,
|
||||
mimetype,
|
||||
onProgress,
|
||||
@@ -109,11 +110,11 @@ Completer<SnCloudFile?> putMediaToCloud({
|
||||
}
|
||||
}
|
||||
|
||||
// If not an image or on web, continue with normal upload
|
||||
_processUpload(
|
||||
fileData,
|
||||
atk,
|
||||
baseUrl,
|
||||
poolId,
|
||||
filename,
|
||||
mimetype,
|
||||
onProgress,
|
||||
@@ -127,6 +128,7 @@ Completer<SnCloudFile?> _processUpload(
|
||||
UniversalFile fileData,
|
||||
String atk,
|
||||
String baseUrl,
|
||||
String? poolId,
|
||||
String? filename,
|
||||
String? mimetype,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
@@ -168,26 +170,81 @@ Completer<SnCloudFile?> _processUpload(
|
||||
return completer;
|
||||
}
|
||||
|
||||
final Map<String, String> metadata = {
|
||||
'filename': actualFilename,
|
||||
'content-type': actualMimetype,
|
||||
};
|
||||
// Create Dio instance
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
headers: {
|
||||
'Authorization': 'AtField $atk',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final client = TusClient(file);
|
||||
client
|
||||
.upload(
|
||||
uri: Uri.parse('$baseUrl/drive/tus'),
|
||||
headers: {'Authorization': 'AtField $atk'},
|
||||
metadata: metadata,
|
||||
onComplete: (lastResponse) {
|
||||
final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!);
|
||||
completer.complete(SnCloudFile.fromJson(resp));
|
||||
},
|
||||
onProgress: (double progress, Duration estimate) {
|
||||
onProgress?.call(progress, estimate);
|
||||
},
|
||||
)
|
||||
.catchError(completer.completeError);
|
||||
final uploader = FileUploader(dio);
|
||||
|
||||
// Get File object
|
||||
File fileObj;
|
||||
if (file.path.isNotEmpty) {
|
||||
fileObj = File(file.path);
|
||||
// Call progress start
|
||||
onProgress?.call(0.0, Duration.zero);
|
||||
uploader
|
||||
.uploadFile(
|
||||
file: fileObj,
|
||||
fileName: actualFilename,
|
||||
contentType: actualMimetype,
|
||||
poolId: poolId,
|
||||
)
|
||||
.then((result) {
|
||||
// Call progress end
|
||||
onProgress?.call(1.0, Duration.zero);
|
||||
completer.complete(result);
|
||||
})
|
||||
.catchError((e) {
|
||||
completer.completeError(e);
|
||||
throw e;
|
||||
});
|
||||
} else {
|
||||
// Write to temp file
|
||||
getTemporaryDirectory()
|
||||
.then((tempDir) {
|
||||
final tempFile = File('${tempDir.path}/temp_upload_$actualFilename');
|
||||
file
|
||||
.readAsBytes()
|
||||
.then((bytes) => tempFile.writeAsBytes(bytes))
|
||||
.then((_) {
|
||||
fileObj = tempFile;
|
||||
// Call progress start
|
||||
onProgress?.call(0.0, Duration.zero);
|
||||
uploader
|
||||
.uploadFile(
|
||||
file: fileObj,
|
||||
fileName: actualFilename,
|
||||
contentType: actualMimetype,
|
||||
poolId: poolId,
|
||||
)
|
||||
.then((result) {
|
||||
// Call progress end
|
||||
onProgress?.call(1.0, Duration.zero);
|
||||
completer.complete(result);
|
||||
})
|
||||
.catchError((e) {
|
||||
completer.completeError(e);
|
||||
throw e;
|
||||
});
|
||||
})
|
||||
.catchError((e) {
|
||||
completer.completeError(e);
|
||||
throw e;
|
||||
});
|
||||
})
|
||||
.catchError((e) {
|
||||
completer.completeError(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
return completer;
|
||||
}
|
||||
|
155
lib/services/file_uploader.dart
Normal file
155
lib/services/file_uploader.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
class FileUploader {
|
||||
final Dio _dio;
|
||||
|
||||
FileUploader(this._dio);
|
||||
|
||||
/// Calculates the MD5 hash of a file.
|
||||
Future<String> _calculateFileHash(File file) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
final digest = md5.convert(bytes);
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
/// Creates an upload task for the given file.
|
||||
Future<Map<String, dynamic>> createUploadTask({
|
||||
required File file,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? chunkSize,
|
||||
}) async {
|
||||
final hash = await _calculateFileHash(file);
|
||||
final fileSize = await file.length();
|
||||
|
||||
final response = await _dio.post(
|
||||
'/drive/files/upload/create',
|
||||
data: {
|
||||
'hash': hash,
|
||||
'file_name': fileName,
|
||||
'file_size': fileSize,
|
||||
'content_type': contentType,
|
||||
'pool_id': poolId,
|
||||
'bundle_id': bundleId,
|
||||
'encrypt_password': encryptPassword,
|
||||
'expired_at': expiredAt,
|
||||
'chunk_size': chunkSize,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/// Uploads a single chunk of the file.
|
||||
Future<void> uploadChunk({
|
||||
required String taskId,
|
||||
required int chunkIndex,
|
||||
required Uint8List chunkData,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({
|
||||
'chunk': MultipartFile.fromBytes(
|
||||
chunkData,
|
||||
filename: 'chunk_$chunkIndex',
|
||||
),
|
||||
});
|
||||
|
||||
await _dio.post(
|
||||
'/drive/files/upload/chunk/$taskId/$chunkIndex',
|
||||
data: formData,
|
||||
);
|
||||
}
|
||||
|
||||
/// Completes the upload and returns the CloudFile object.
|
||||
Future<SnCloudFile> completeUpload(String taskId) async {
|
||||
final response = await _dio.post('/drive/files/upload/complete/$taskId');
|
||||
|
||||
return SnCloudFile.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Uploads a file in chunks using the multi-part API.
|
||||
Future<SnCloudFile> uploadFile({
|
||||
required File file,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? customChunkSize,
|
||||
}) async {
|
||||
// Step 1: Create upload task
|
||||
final createResponse = await createUploadTask(
|
||||
file: file,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
poolId: poolId,
|
||||
bundleId: bundleId,
|
||||
encryptPassword: encryptPassword,
|
||||
expiredAt: expiredAt,
|
||||
chunkSize: customChunkSize,
|
||||
);
|
||||
|
||||
if (createResponse['file_exists'] == true) {
|
||||
// File already exists, return the existing file
|
||||
return SnCloudFile.fromJson(createResponse['file']);
|
||||
}
|
||||
|
||||
final taskId = createResponse['task_id'] as String;
|
||||
final chunkSize = createResponse['chunk_size'] as int;
|
||||
final chunksCount = createResponse['chunks_count'] as int;
|
||||
|
||||
// Step 2: Upload chunks
|
||||
final stream = file.openRead();
|
||||
final chunks = <Uint8List>[];
|
||||
int bytesRead = 0;
|
||||
final buffer = BytesBuilder();
|
||||
|
||||
await for (final chunk in stream) {
|
||||
buffer.add(chunk);
|
||||
bytesRead += chunk.length;
|
||||
|
||||
if (bytesRead >= chunkSize) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
bytesRead = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining bytes as last chunk
|
||||
if (buffer.length > 0) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
}
|
||||
|
||||
// Ensure we have the correct number of chunks
|
||||
if (chunks.length != chunksCount) {
|
||||
throw Exception(
|
||||
'Chunk count mismatch: expected $chunksCount, got ${chunks.length}',
|
||||
);
|
||||
}
|
||||
|
||||
// Upload each chunk
|
||||
for (int i = 0; i < chunks.length; i++) {
|
||||
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
|
||||
}
|
||||
|
||||
// Step 3: Complete upload
|
||||
return await completeUpload(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
// Riverpod provider for the FileUploader service
|
||||
final fileUploaderProvider = Provider<FileUploader>((ref) {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
return FileUploader(dio);
|
||||
});
|
@@ -1,230 +1,47 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
|
||||
|
||||
void _onAppLifecycleChanged(AppLifecycleState state) {
|
||||
_appLifecycleState = state;
|
||||
}
|
||||
// Conditional imports based on platform
|
||||
import 'notify.windows.dart' as windows_notify;
|
||||
import 'notify.universal.dart' as universal_notify;
|
||||
|
||||
// Platform-specific delegation
|
||||
Future<void> initializeLocalNotifications() async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsIOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsMacOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
|
||||
const WindowsInitializationSettings initializationSettingsWindows =
|
||||
WindowsInitializationSettings(
|
||||
appName: 'Island',
|
||||
appUserModelId: 'dev.solsynth.solian',
|
||||
guid: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
const InitializationSettings initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsIOS,
|
||||
macOS: initializationSettingsMacOS,
|
||||
linux: initializationSettingsLinux,
|
||||
windows: initializationSettingsWindows,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {
|
||||
final payload = response.payload;
|
||||
if (payload != null) {
|
||||
if (payload.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(payload);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addObserver(
|
||||
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
|
||||
);
|
||||
}
|
||||
|
||||
class LifecycleEventHandler extends WidgetsBindingObserver {
|
||||
final void Function(AppLifecycleState) onAppLifecycleChanged;
|
||||
|
||||
LifecycleEventHandler({required this.onAppLifecycleChanged});
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onAppLifecycleChanged(state);
|
||||
if (Platform.isWindows) {
|
||||
return windows_notify.initializeLocalNotifications();
|
||||
} else {
|
||||
return universal_notify.initializeLocalNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
StreamSubscription setupNotificationListener(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
return ws.dataStream.listen((pkt) async {
|
||||
if (pkt.type == "notifications.new") {
|
||||
final notification = SnNotification.fromJson(pkt.data!);
|
||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||
// App is focused, show in-app notification
|
||||
log(
|
||||
'[Notification] Showing in-app notification: ${notification.title}',
|
||||
);
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: NotificationCard(notification: notification),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
snackBarPosition: SnackBarPosition.top,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top:
|
||||
(!kIsWeb &&
|
||||
(Platform.isMacOS ||
|
||||
Platform.isWindows ||
|
||||
Platform.isLinux))
|
||||
? 28
|
||||
// ignore: use_build_context_synchronously
|
||||
: MediaQuery.of(context).padding.top + 16,
|
||||
bottom: 16,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// App is in background, show system notification (only on supported platforms)
|
||||
if (!kIsWeb && !Platform.isIOS) {
|
||||
log(
|
||||
'[Notification] Showing system notification: ${notification.title}',
|
||||
);
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'channel_id',
|
||||
'channel_name',
|
||||
channelDescription: 'channel_description',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: 'ticker',
|
||||
);
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
);
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
notification.title,
|
||||
notification.content,
|
||||
notificationDetails,
|
||||
payload: notification.meta['action_uri'] as String?,
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'[Notification] Skipping system notification for unsupported platform: ${notification.title}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (Platform.isWindows) {
|
||||
return windows_notify.setupNotificationListener(context, ref);
|
||||
} else {
|
||||
return universal_notify.setupNotificationListener(context, ref);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> subscribePushNotification(
|
||||
Dio apiClient, {
|
||||
bool detailedErrors = false,
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
return;
|
||||
}
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
String? deviceToken;
|
||||
if (kIsWeb) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken(
|
||||
vapidKey:
|
||||
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken();
|
||||
} else if (Platform.isIOS) {
|
||||
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh
|
||||
.listen((fcmToken) {
|
||||
_putTokenToRemote(apiClient, fcmToken, 1);
|
||||
})
|
||||
.onError((err) {
|
||||
log("Failed to get firebase cloud messaging push token: $err");
|
||||
});
|
||||
|
||||
if (deviceToken != null) {
|
||||
_putTokenToRemote(
|
||||
if (Platform.isWindows) {
|
||||
return windows_notify.subscribePushNotification(
|
||||
apiClient,
|
||||
deviceToken,
|
||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
|
||||
detailedErrors: detailedErrors,
|
||||
);
|
||||
} else {
|
||||
return universal_notify.subscribePushNotification(
|
||||
apiClient,
|
||||
detailedErrors: detailedErrors,
|
||||
);
|
||||
} else if (detailedErrors) {
|
||||
throw Exception("Failed to get device token for push notifications.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _putTokenToRemote(
|
||||
Dio apiClient,
|
||||
String token,
|
||||
int provider,
|
||||
) async {
|
||||
await apiClient.put(
|
||||
"/pusher/notifications/subscription",
|
||||
data: {"provider": provider, "device_token": token},
|
||||
);
|
||||
}
|
||||
|
232
lib/services/notify.universal.dart
Normal file
232
lib/services/notify.universal.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
|
||||
|
||||
void _onAppLifecycleChanged(AppLifecycleState state) {
|
||||
_appLifecycleState = state;
|
||||
}
|
||||
|
||||
Future<void> initializeLocalNotifications() async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsIOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsMacOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
|
||||
const WindowsInitializationSettings initializationSettingsWindows =
|
||||
WindowsInitializationSettings(
|
||||
appName: 'Island',
|
||||
appUserModelId: 'dev.solsynth.solian',
|
||||
guid: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
const InitializationSettings initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsIOS,
|
||||
macOS: initializationSettingsMacOS,
|
||||
linux: initializationSettingsLinux,
|
||||
windows: initializationSettingsWindows,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {
|
||||
final payload = response.payload;
|
||||
if (payload != null) {
|
||||
if (payload.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(payload);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addObserver(
|
||||
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
|
||||
);
|
||||
}
|
||||
|
||||
class LifecycleEventHandler extends WidgetsBindingObserver {
|
||||
final void Function(AppLifecycleState) onAppLifecycleChanged;
|
||||
|
||||
LifecycleEventHandler({required this.onAppLifecycleChanged});
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onAppLifecycleChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
return ws.dataStream.listen((pkt) async {
|
||||
if (pkt.type == "notifications.new") {
|
||||
final notification = SnNotification.fromJson(pkt.data!);
|
||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||
// App is focused, show in-app notification
|
||||
log(
|
||||
'[Notification] Showing in-app notification: ${notification.title}',
|
||||
);
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: NotificationCard(notification: notification),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
snackBarPosition: SnackBarPosition.top,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top:
|
||||
(!kIsWeb &&
|
||||
(Platform.isMacOS ||
|
||||
Platform.isWindows ||
|
||||
Platform.isLinux))
|
||||
? 28
|
||||
// ignore: use_build_context_synchronously
|
||||
: MediaQuery.of(context).padding.top + 16,
|
||||
bottom: 16,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// App is in background, show system notification (only on supported platforms)
|
||||
if (!kIsWeb && !Platform.isIOS) {
|
||||
log(
|
||||
'[Notification] Showing system notification: ${notification.title}',
|
||||
);
|
||||
|
||||
// Use flutter_local_notifications for universal platforms
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'channel_id',
|
||||
'channel_name',
|
||||
channelDescription: 'channel_description',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: 'ticker',
|
||||
);
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
);
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
notification.title,
|
||||
notification.content,
|
||||
notificationDetails,
|
||||
payload: notification.meta['action_uri'] as String?,
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'[Notification] Skipping system notification for unsupported platform: ${notification.title}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> subscribePushNotification(
|
||||
Dio apiClient, {
|
||||
bool detailedErrors = false,
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
return;
|
||||
}
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
String? deviceToken;
|
||||
if (kIsWeb) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken(
|
||||
vapidKey:
|
||||
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken();
|
||||
} else if (Platform.isIOS) {
|
||||
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh
|
||||
.listen((fcmToken) {
|
||||
_putTokenToRemote(apiClient, fcmToken, 1);
|
||||
})
|
||||
.onError((err) {
|
||||
log("Failed to get firebase cloud messaging push token: $err");
|
||||
});
|
||||
|
||||
if (deviceToken != null) {
|
||||
_putTokenToRemote(
|
||||
apiClient,
|
||||
deviceToken,
|
||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
|
||||
);
|
||||
} else if (detailedErrors) {
|
||||
throw Exception("Failed to get device token for push notifications.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _putTokenToRemote(
|
||||
Dio apiClient,
|
||||
String token,
|
||||
int provider,
|
||||
) async {
|
||||
await apiClient.put(
|
||||
"/ring/notifications/subscription",
|
||||
data: {"provider": provider, "device_token": token},
|
||||
);
|
||||
}
|
176
lib/services/notify.windows.dart
Normal file
176
lib/services/notify.windows.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:windows_notification/windows_notification.dart'
|
||||
as windows_notification;
|
||||
import 'package:windows_notification/notification_message.dart';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
// Windows notification instance
|
||||
windows_notification.WindowsNotification? windowsNotification;
|
||||
|
||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
|
||||
|
||||
void _onAppLifecycleChanged(AppLifecycleState state) {
|
||||
_appLifecycleState = state;
|
||||
}
|
||||
|
||||
Future<void> initializeLocalNotifications() async {
|
||||
// Initialize Windows notification for Windows platform
|
||||
windowsNotification = windows_notification.WindowsNotification(
|
||||
applicationId: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addObserver(
|
||||
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
|
||||
);
|
||||
}
|
||||
|
||||
class LifecycleEventHandler extends WidgetsBindingObserver {
|
||||
final void Function(AppLifecycleState) onAppLifecycleChanged;
|
||||
|
||||
LifecycleEventHandler({required this.onAppLifecycleChanged});
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onAppLifecycleChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
return ws.dataStream.listen((pkt) async {
|
||||
if (pkt.type == "notifications.new") {
|
||||
final notification = SnNotification.fromJson(pkt.data!);
|
||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||
// App is focused, show in-app notification
|
||||
log(
|
||||
'[Notification] Showing in-app notification: ${notification.title}',
|
||||
);
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: NotificationCard(notification: notification),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
snackBarPosition: SnackBarPosition.top,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 28, // Windows specific padding
|
||||
bottom: 16,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// App is in background, show Windows system notification
|
||||
log(
|
||||
'[Notification] Showing Windows system notification: ${notification.title}',
|
||||
);
|
||||
|
||||
if (windowsNotification != null) {
|
||||
// Use Windows notification for Windows platform
|
||||
final notificationMessage = NotificationMessage.fromPluginTemplate(
|
||||
DateTime.now().millisecondsSinceEpoch.toString(), // unique id
|
||||
notification.title,
|
||||
notification.content,
|
||||
launch: notification.meta['action_uri'] as String?,
|
||||
);
|
||||
await windowsNotification!.showNotificationPluginTemplate(
|
||||
notificationMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> subscribePushNotification(
|
||||
Dio apiClient, {
|
||||
bool detailedErrors = false,
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
return;
|
||||
}
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
String? deviceToken;
|
||||
if (kIsWeb) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken(
|
||||
vapidKey:
|
||||
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken();
|
||||
} else if (Platform.isIOS) {
|
||||
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh
|
||||
.listen((fcmToken) {
|
||||
_putTokenToRemote(apiClient, fcmToken, 1);
|
||||
})
|
||||
.onError((err) {
|
||||
log("Failed to get firebase cloud messaging push token: $err");
|
||||
});
|
||||
|
||||
if (deviceToken != null) {
|
||||
_putTokenToRemote(
|
||||
apiClient,
|
||||
deviceToken,
|
||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
|
||||
);
|
||||
} else if (detailedErrors) {
|
||||
throw Exception("Failed to get device token for push notifications.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _putTokenToRemote(
|
||||
Dio apiClient,
|
||||
String token,
|
||||
int provider,
|
||||
) async {
|
||||
await apiClient.put(
|
||||
"/ring/notifications/subscription",
|
||||
data: {"provider": provider, "device_token": token},
|
||||
);
|
||||
}
|
@@ -14,7 +14,7 @@ Future<void> initializeTzdb() async {
|
||||
}
|
||||
|
||||
Future<String> getMachineTz() async {
|
||||
return await FlutterTimezone.getLocalTimezone();
|
||||
return (await FlutterTimezone.getLocalTimezone()).identifier;
|
||||
}
|
||||
|
||||
List<String> getAvailableTz() {
|
||||
|
@@ -13,7 +13,7 @@ Future<void> initializeTzdb() async {
|
||||
}
|
||||
|
||||
Future<String> getMachineTz() async {
|
||||
return await FlutterTimezone.getLocalTimezone();
|
||||
return (await FlutterTimezone.getLocalTimezone()).identifier;
|
||||
}
|
||||
|
||||
List<String> getAvailableTz() {
|
||||
|
@@ -1 +1,3 @@
|
||||
export 'udid.native.dart' if (dart.library.html) 'udid.web.dart';
|
||||
export 'udid.native.dart'
|
||||
if (dart.library.html) 'udid.web.dart'
|
||||
if (dart.library.io) 'udid.native.dart';
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
|
||||
String? _cachedUdid;
|
||||
@@ -9,3 +12,18 @@ Future<String> getUdid() async {
|
||||
_cachedUdid = await FlutterUdid.consistentUdid;
|
||||
return _cachedUdid!;
|
||||
}
|
||||
|
||||
Future<String> getDeviceName() async {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
return androidInfo.device;
|
||||
} else if (Platform.isIOS) {
|
||||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||
return iosInfo.name;
|
||||
} else if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
|
||||
return Platform.localHostname;
|
||||
} else {
|
||||
return 'unknown'.tr();
|
||||
}
|
||||
}
|
||||
|
@@ -9,3 +9,18 @@ Future<String> getUdid() async {
|
||||
final hash = sha256.convert(bytes);
|
||||
return hash.toString();
|
||||
}
|
||||
|
||||
Future<String> getDeviceName() async {
|
||||
final userAgent = window.navigator.userAgent;
|
||||
if (userAgent.contains('Chrome') && !userAgent.contains('Edg')) {
|
||||
return 'Chrome';
|
||||
} else if (userAgent.contains('Firefox')) {
|
||||
return 'Firefox';
|
||||
} else if (userAgent.contains('Safari') && !userAgent.contains('Chrome')) {
|
||||
return 'Safari';
|
||||
} else if (userAgent.contains('Edg')) {
|
||||
return 'Edge';
|
||||
} else {
|
||||
return 'Browser';
|
||||
}
|
||||
}
|
||||
|
122
lib/utils/activity_utils.dart
Normal file
122
lib/utils/activity_utils.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
|
||||
String? getActivityTitle(String? label, Map<String, dynamic>? meta) {
|
||||
if (meta == null) return label;
|
||||
if (meta['assets']?['large_text'] is String) {
|
||||
return meta['assets']?['large_text'];
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
String? getActivitySubtitle(Map<String, dynamic>? meta) {
|
||||
if (meta == null) return null;
|
||||
if (meta['assets']?['small_text'] is String) {
|
||||
return meta['assets']?['small_text'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
InlineSpan getActivityFullMessage(SnAccountStatus? status) {
|
||||
if (status?.meta == null) return TextSpan(text: 'No activity details available');
|
||||
final meta = status!.meta!;
|
||||
final List<InlineSpan> spans = [];
|
||||
if (meta.containsKey('assets') && meta['assets'] is Map) {
|
||||
final assets = meta['assets'] as Map<String, dynamic>;
|
||||
if (assets.containsKey('large_text')) {
|
||||
spans.add(TextSpan(text: assets['large_text'], style: TextStyle(fontWeight: FontWeight.bold)));
|
||||
}
|
||||
if (assets.containsKey('small_text')) {
|
||||
if (spans.isNotEmpty) spans.add(TextSpan(text: '\n'));
|
||||
spans.add(TextSpan(text: assets['small_text']));
|
||||
}
|
||||
}
|
||||
String normalText = '';
|
||||
if (meta.containsKey('details')) {
|
||||
normalText += 'Details: ${meta['details']}\n';
|
||||
}
|
||||
if (meta.containsKey('state')) {
|
||||
normalText += 'State: ${meta['state']}\n';
|
||||
}
|
||||
if (meta.containsKey('timestamps') && meta['timestamps'] is Map) {
|
||||
final ts = meta['timestamps'] as Map<String, dynamic>;
|
||||
if (ts.containsKey('start') && ts['start'] is int) {
|
||||
final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000);
|
||||
normalText += 'Started: ${start.toLocal()}\n';
|
||||
}
|
||||
if (ts.containsKey('end') && ts['end'] is int) {
|
||||
final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000);
|
||||
normalText += 'Ends: ${end.toLocal()}\n';
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('party') && meta['party'] is Map) {
|
||||
final party = meta['party'] as Map<String, dynamic>;
|
||||
if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) {
|
||||
final size = party['size'] as List;
|
||||
normalText += 'Party: ${size[0]}/${size[1]}\n';
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('instance')) {
|
||||
normalText += 'Instance: ${meta['instance']}\n';
|
||||
}
|
||||
// Add other keys if present
|
||||
meta.forEach((key, value) {
|
||||
if (!['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(key)) {
|
||||
normalText += '$key: $value\n';
|
||||
}
|
||||
});
|
||||
if (normalText.isNotEmpty) {
|
||||
if (spans.isNotEmpty) spans.add(TextSpan(text: '\n'));
|
||||
spans.add(TextSpan(text: normalText.trimRight()));
|
||||
}
|
||||
return TextSpan(children: spans);
|
||||
}
|
||||
|
||||
Widget buildActivityDetails(SnAccountStatus? status) {
|
||||
if (status?.meta == null) return Text('No activity details available');
|
||||
final meta = status!.meta!;
|
||||
final List<Widget> children = [];
|
||||
if (meta.containsKey('assets') && meta['assets'] is Map) {
|
||||
final assets = meta['assets'] as Map<String, dynamic>;
|
||||
if (assets.containsKey('large_text')) {
|
||||
children.add(Text(assets['large_text']));
|
||||
}
|
||||
if (assets.containsKey('small_text')) {
|
||||
children.add(Text(assets['small_text']));
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('details')) {
|
||||
children.add(Text('Details: ${meta['details']}'));
|
||||
}
|
||||
if (meta.containsKey('state')) {
|
||||
children.add(Text('State: ${meta['state']}'));
|
||||
}
|
||||
if (meta.containsKey('timestamps') && meta['timestamps'] is Map) {
|
||||
final ts = meta['timestamps'] as Map<String, dynamic>;
|
||||
if (ts.containsKey('start') && ts['start'] is int) {
|
||||
final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000);
|
||||
children.add(Text('Started: ${start.toLocal()}'));
|
||||
}
|
||||
if (ts.containsKey('end') && ts['end'] is int) {
|
||||
final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000);
|
||||
children.add(Text('Ends: ${end.toLocal()}'));
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('party') && meta['party'] is Map) {
|
||||
final party = meta['party'] as Map<String, dynamic>;
|
||||
if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) {
|
||||
final size = party['size'] as List;
|
||||
children.add(Text('Party: ${size[0]}/${size[1]}'));
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('instance')) {
|
||||
children.add(Text('Instance: ${meta['instance']}'));
|
||||
}
|
||||
// Add other keys if present
|
||||
children.addAll(meta.entries.where((e) => !['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(e.key)).map((e) => Text('${e.key}: ${e.value}')));
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
);
|
||||
}
|
@@ -6,10 +6,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/services/udid.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
@@ -43,32 +45,11 @@ class _DeviceListTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: Icon(switch (device.platform) {
|
||||
0 => Icons.device_unknown, // Unidentified
|
||||
1 => Icons.web, // Web
|
||||
2 => Icons.phone_iphone, // iOS
|
||||
3 => Icons.phone_android, // Android
|
||||
4 => Icons.laptop_mac, // macOS
|
||||
5 => Icons.window, // Windows
|
||||
6 => Icons.computer, // Linux
|
||||
_ => Icons.device_unknown, // fallback
|
||||
}).padding(top: 4),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
return ExpansionTile(
|
||||
title: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
'lastActiveAt'.tr(
|
||||
args: [
|
||||
DateFormat().format(
|
||||
device.challenges.first.createdAt.toLocal(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(device.challenges.first.ipAddress),
|
||||
Flexible(child: Text(device.deviceLabel ?? device.deviceName)),
|
||||
if (device.isCurrent)
|
||||
Row(
|
||||
children: [
|
||||
@@ -82,10 +63,29 @@ class _DeviceListTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(top: 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(device.deviceLabel ?? device.deviceName),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'lastActiveAt'.tr(
|
||||
args: [device.challenges.first.createdAt.formatSystem()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: Icon(switch (device.platform) {
|
||||
0 => Icons.device_unknown, // Unidentified
|
||||
1 => Icons.web, // Web
|
||||
2 => Icons.phone_iphone, // iOS
|
||||
3 => Icons.phone_android, // Android
|
||||
4 => Icons.laptop_mac, // macOS
|
||||
5 => Icons.window, // Windows
|
||||
6 => Icons.computer, // Linux
|
||||
_ => Icons.device_unknown, // fallback
|
||||
}).padding(top: 4),
|
||||
trailing:
|
||||
isWideScreen(context)
|
||||
? Row(
|
||||
@@ -105,6 +105,36 @@ class _DeviceListTile extends StatelessWidget {
|
||||
],
|
||||
)
|
||||
: null,
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text('authDeviceChallenges'.tr()),
|
||||
),
|
||||
for (final challenge in device.challenges)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text(DateFormat().format(challenge.createdAt.toLocal())),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(challenge.ipAddress),
|
||||
if (challenge.location != null)
|
||||
Row(
|
||||
spacing: 4,
|
||||
children:
|
||||
[challenge.location?.city, challenge.location?.country]
|
||||
.where((e) => e?.isNotEmpty ?? false)
|
||||
.map((e) => Text(e!))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -176,72 +206,117 @@ class AccountSessionSheet extends HookConsumerWidget {
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'authSessions'.tr(),
|
||||
child: authDevices.when(
|
||||
data:
|
||||
(data) => ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync(() => ref.invalidate(authDevicesProvider)),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final device = data[index];
|
||||
if (wideScreen) {
|
||||
return _DeviceListTile(
|
||||
device: device,
|
||||
updateDeviceLabel: updateDeviceLabel,
|
||||
logoutDevice: logoutDevice,
|
||||
);
|
||||
} else {
|
||||
return Dismissible(
|
||||
key: Key('device-${device.id}'),
|
||||
direction:
|
||||
device.isCurrent
|
||||
? DismissDirection.startToEnd
|
||||
: DismissDirection.horizontal,
|
||||
background: Container(
|
||||
color: Colors.blue,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Icon(Icons.edit, color: Colors.white),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!wideScreen)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(Symbols.info, size: 16).padding(top: 2),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'authDeviceHint'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Icon(Icons.logout, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (direction) async {
|
||||
if (direction == DismissDirection.startToEnd) {
|
||||
updateDeviceLabel(device.deviceId);
|
||||
return false;
|
||||
} else {
|
||||
final confirm = await showConfirmAlert(
|
||||
'authDeviceLogoutHint'.tr(),
|
||||
'authDeviceLogout'.tr(),
|
||||
);
|
||||
if (confirm && context.mounted) {
|
||||
logoutDevice(device.deviceId);
|
||||
}
|
||||
return false; // Don't dismiss
|
||||
}
|
||||
},
|
||||
child: _DeviceListTile(
|
||||
device: device,
|
||||
updateDeviceLabel: updateDeviceLabel,
|
||||
logoutDevice: logoutDevice,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
error:
|
||||
(err, _) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(authDevicesProvider),
|
||||
Expanded(
|
||||
child: authDevices.when(
|
||||
data:
|
||||
(data) => ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync(
|
||||
() => ref.invalidate(authDevicesProvider),
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final device = data[index];
|
||||
if (wideScreen) {
|
||||
return _DeviceListTile(
|
||||
device: device,
|
||||
updateDeviceLabel: updateDeviceLabel,
|
||||
logoutDevice: logoutDevice,
|
||||
);
|
||||
} else {
|
||||
return Dismissible(
|
||||
key: Key('device-${device.id}'),
|
||||
direction:
|
||||
device.isCurrent
|
||||
? DismissDirection.startToEnd
|
||||
: DismissDirection.horizontal,
|
||||
background: Container(
|
||||
color: Colors.blue,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Icon(Icons.edit, color: Colors.white),
|
||||
),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Icon(Icons.logout, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (direction) async {
|
||||
if (direction == DismissDirection.startToEnd) {
|
||||
updateDeviceLabel(device.deviceId);
|
||||
return false;
|
||||
} else {
|
||||
final confirm = await showConfirmAlert(
|
||||
'authDeviceLogoutHint'.tr(),
|
||||
'authDeviceLogout'.tr(),
|
||||
);
|
||||
if (confirm && context.mounted) {
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final apiClient = ref.watch(
|
||||
apiClientProvider,
|
||||
);
|
||||
await apiClient.delete(
|
||||
'/id/accounts/me/devices/${device.deviceId}',
|
||||
);
|
||||
ref.invalidate(authDevicesProvider);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return confirm;
|
||||
}
|
||||
},
|
||||
child: _DeviceListTile(
|
||||
device: device,
|
||||
updateDeviceLabel: updateDeviceLabel,
|
||||
logoutDevice: logoutDevice,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
error:
|
||||
(err, _) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(authDevicesProvider),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -147,6 +147,7 @@ class AccountProfileCard extends HookConsumerWidget {
|
||||
if (data.badges.isNotEmpty)
|
||||
BadgeList(badges: data.badges).padding(top: 12),
|
||||
LevelingProgressCard(
|
||||
isCompact: true,
|
||||
level: data.profile.level,
|
||||
experience: data.profile.experience,
|
||||
progress: data.profile.levelingProgress,
|
||||
|
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/models/activity.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/utils/activity_utils.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -75,7 +76,10 @@ class EventDetailsWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(status.label),
|
||||
if ((getActivityTitle(status.label, status.meta) ?? status.label).isNotEmpty)
|
||||
Text(getActivityTitle(status.label, status.meta) ?? status.label),
|
||||
if (getActivitySubtitle(status.meta) != null)
|
||||
Text(getActivitySubtitle(status.meta)!).fontSize(11).opacity(0.8),
|
||||
Text(
|
||||
'${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}',
|
||||
).fontSize(11).opacity(0.8),
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -8,50 +7,152 @@ class LevelingProgressCard extends StatelessWidget {
|
||||
final int level;
|
||||
final int experience;
|
||||
final double progress;
|
||||
final VoidCallback? onTap;
|
||||
final bool isCompact;
|
||||
|
||||
const LevelingProgressCard({
|
||||
super.key,
|
||||
required this.level,
|
||||
required this.experience,
|
||||
required this.progress,
|
||||
this.onTap,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
// Calculate level stage (1-12, each stage covers 10 levels)
|
||||
int stage = ((level - 1) ~/ 10) + 1;
|
||||
stage = stage.clamp(1, 12); // Ensure stage is within 1-12
|
||||
|
||||
// Define colors for each stage
|
||||
const List<Color> stageColors = [
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.teal,
|
||||
Colors.cyan,
|
||||
Colors.indigo,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.pink,
|
||||
Colors.red,
|
||||
];
|
||||
|
||||
Color stageColor = stageColors[stage - 1];
|
||||
|
||||
// Compact mode adjustments
|
||||
final double levelFontSize = isCompact ? 14 : 18;
|
||||
final double stageFontSize = isCompact ? 13 : 14;
|
||||
final double experienceFontSize = isCompact ? 12 : 14;
|
||||
final double progressHeight = isCompact ? 6 : 10;
|
||||
final double horizontalPadding = isCompact ? 16 : 20;
|
||||
final double verticalPadding = isCompact ? 12 : 16;
|
||||
final double gapSize = isCompact ? 4 : 8;
|
||||
final double rowSpacing = 12;
|
||||
|
||||
final cardContent = Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
'levelingProgressLevel'.tr(args: [level.toString()]),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
).fontSize(13).bold(),
|
||||
Text(
|
||||
'levelingProgressExperience'.tr(args: [experience.toString()]),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
).fontSize(13),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
Tooltip(
|
||||
message: '${(progress).toStringAsFixed(1)}%',
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 4,
|
||||
value: progress / 100,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
stageColor.withOpacity(0.1),
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
spacing: rowSpacing,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'levelingProgressLevel'.tr(args: [level.toString()]),
|
||||
style: TextStyle(
|
||||
color: stageColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: levelFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'levelingStage$stage'.tr(),
|
||||
style: TextStyle(
|
||||
color: stageColor.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: stageFontSize,
|
||||
),
|
||||
),
|
||||
if (onTap != null) ...[
|
||||
const Gap(4),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: isCompact ? 10 : 12,
|
||||
color: stageColor.withOpacity(0.7),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Gap(gapSize),
|
||||
Row(
|
||||
spacing: rowSpacing,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: '${progress.toStringAsFixed(1)}%',
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: progressHeight,
|
||||
value: progress,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow.withOpacity(0.75),
|
||||
color: stageColor,
|
||||
stopIndicatorRadius: 0,
|
||||
trackGap: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'levelingProgressExperience'.tr(
|
||||
args: [experience.toString()],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.8),
|
||||
fontSize: experienceFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: horizontalPadding, vertical: verticalPadding),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return cardContent;
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/utils/activity_utils.dart';
|
||||
import 'package:island/widgets/account/status_creation.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -13,8 +15,31 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'status.g.dart';
|
||||
|
||||
class CurrentAccountStatusNotifier extends StateNotifier<SnAccountStatus?> {
|
||||
CurrentAccountStatusNotifier() : super(null);
|
||||
|
||||
void setStatus(SnAccountStatus status) {
|
||||
state = status;
|
||||
}
|
||||
|
||||
void clearStatus() {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
|
||||
final currentAccountStatusProvider = StateNotifierProvider<CurrentAccountStatusNotifier, SnAccountStatus?>((ref) {
|
||||
return CurrentAccountStatusNotifier();
|
||||
});
|
||||
|
||||
@riverpod
|
||||
Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async {
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
if (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name)) {
|
||||
final local = ref.watch(currentAccountStatusProvider);
|
||||
if (local != null) {
|
||||
return local;
|
||||
}
|
||||
}
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
try {
|
||||
final resp = await apiClient.get('/id/accounts/$uname/statuses');
|
||||
@@ -110,7 +135,11 @@ class AccountStatusWidget extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final status = ref.watch(accountStatusProvider(uname));
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
final localStatus = ref.watch(currentAccountStatusProvider);
|
||||
final status = (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name && localStatus != null))
|
||||
? AsyncValue.data(localStatus)
|
||||
: ref.watch(accountStatusProvider(uname));
|
||||
final account = ref.watch(accountProvider(uname));
|
||||
|
||||
return Padding(
|
||||
@@ -133,10 +162,31 @@ class AccountStatusWidget extends HookConsumerWidget {
|
||||
).padding(right: 4),
|
||||
if (status.value?.isCustomized ?? false)
|
||||
Flexible(
|
||||
child: Text(
|
||||
status.value?.label ?? 'unknown'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Activity Details'),
|
||||
content: buildActivityDetails(status.value),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Close'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Tooltip(
|
||||
richMessage: getActivityFullMessage(status.value),
|
||||
child: Text(
|
||||
getActivityTitle(status.value?.label, status.value?.meta) ??
|
||||
'unknown'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
@@ -148,7 +198,13 @@ class AccountStatusWidget extends HookConsumerWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
),
|
||||
if (!(status.value?.isOnline ?? false) &&
|
||||
if (getActivitySubtitle(status.value?.meta) != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
getActivitySubtitle(status.value?.meta)!,
|
||||
).opacity(0.75),
|
||||
)
|
||||
else if (!(status.value?.isOnline ?? false) &&
|
||||
account.value?.profile.lastSeenAt != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
|
@@ -6,7 +6,7 @@ part of 'status.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$accountStatusHash() => r'c861a0565d6229fd35666bba7cb2f5c6b7298e46';
|
||||
String _$accountStatusHash() => r'abc2f11f0fbaf637efc182cf85ab838936c4d875';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -15,6 +14,7 @@ import 'package:island/services/responsive.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class AppScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
@@ -36,11 +36,12 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
void saveWindowSize() {
|
||||
final size = appWindow.size;
|
||||
final settingsNotifier = ref.read(
|
||||
appSettingsNotifierProvider.notifier,
|
||||
);
|
||||
settingsNotifier.setWindowSize(size);
|
||||
windowManager.getBounds().then((bounds) {
|
||||
final settingsNotifier = ref.read(
|
||||
appSettingsNotifierProvider.notifier,
|
||||
);
|
||||
settingsNotifier.setWindowSize(bounds.size);
|
||||
});
|
||||
}
|
||||
|
||||
// Save window size when app is about to close
|
||||
@@ -61,13 +62,6 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final windowButtonColor = WindowButtonColors(
|
||||
iconNormal: Theme.of(context).colorScheme.primary,
|
||||
mouseOver: Theme.of(context).colorScheme.primaryContainer,
|
||||
mouseDown: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
iconMouseOver: Theme.of(context).colorScheme.primary,
|
||||
iconMouseDown: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
|
||||
return Material(
|
||||
child: Stack(
|
||||
@@ -75,44 +69,66 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
WindowTitleBarBox(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
child: MoveWindow(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment:
|
||||
Platform.isMacOS
|
||||
? MainAxisAlignment.center
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Solar Network',
|
||||
textAlign:
|
||||
Platform.isMacOS
|
||||
? TextAlign.center
|
||||
: TextAlign.start,
|
||||
).padding(horizontal: 12, vertical: 5),
|
||||
),
|
||||
child: DragToMoveArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment:
|
||||
Platform.isMacOS
|
||||
? MainAxisAlignment.center
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Solar Network',
|
||||
textAlign:
|
||||
Platform.isMacOS
|
||||
? TextAlign.center
|
||||
: TextAlign.start,
|
||||
).padding(horizontal: 12, vertical: 5),
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
IconButton(
|
||||
icon: Icon(Symbols.minimize),
|
||||
onPressed: () => windowManager.minimize(),
|
||||
iconSize: 16,
|
||||
padding: EdgeInsets.all(8),
|
||||
constraints: BoxConstraints(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
MinimizeWindowButton(colors: windowButtonColor),
|
||||
if (!Platform.isMacOS)
|
||||
MaximizeWindowButton(colors: windowButtonColor),
|
||||
if (!Platform.isMacOS)
|
||||
CloseWindowButton(
|
||||
colors: windowButtonColor,
|
||||
onPressed: () => appWindow.hide(),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
IconButton(
|
||||
icon: Icon(Symbols.maximize),
|
||||
onPressed: () async {
|
||||
if (await windowManager.isMaximized()) {
|
||||
windowManager.restore();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
}
|
||||
},
|
||||
iconSize: 16,
|
||||
padding: EdgeInsets.all(8),
|
||||
constraints: BoxConstraints(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
IconButton(
|
||||
icon: Icon(Symbols.close),
|
||||
onPressed: () => windowManager.close(),
|
||||
iconSize: 16,
|
||||
padding: EdgeInsets.all(8),
|
||||
constraints: BoxConstraints(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/activity_rpc.dart';
|
||||
import 'package:island/pods/activity/activity_rpc.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/screens/tray_manager.dart';
|
||||
import 'package:island/services/notify.dart';
|
||||
@@ -12,6 +11,7 @@ import 'package:island/services/update_service.dart';
|
||||
import 'package:island/widgets/content/network_status_sheet.dart';
|
||||
import 'package:island/widgets/tour/tour.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
final Widget child;
|
||||
@@ -67,11 +67,7 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
}
|
||||
|
||||
void _trayIconPrimaryAction() {
|
||||
if (appWindow.isVisible) {
|
||||
appWindow.restore();
|
||||
} else {
|
||||
appWindow.show();
|
||||
}
|
||||
windowManager.show();
|
||||
}
|
||||
|
||||
void _trayIconSecondaryAction() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user