Compare commits
33 Commits
3.2.0+133
...
6ed6f60fbc
Author | SHA1 | Date | |
---|---|---|---|
6ed6f60fbc | |||
e65a414065 | |||
214d5c4a53 | |||
fe33931304 | |||
113309257e | |||
b95a8b2ed2 | |||
|
e922971a5e | ||
9d5b71bead | |||
890efa2efb | |||
674097e425 | |||
3379dcb7f3
|
|||
eb5a849e1f | |||
4981a23e8e
|
|||
c64d4bacb6
|
|||
838d18013b
|
|||
3f7902e463
|
|||
54560ad5d8
|
|||
0c729db639
|
|||
1fbaac8d88
|
|||
b9dc724f0b
|
|||
a2cc55696f
|
|||
e79f857feb
|
|||
affba29c04
|
|||
756746b144
|
|||
28b6eade48
|
|||
1de7ef8c96 | |||
67eac5dcf5 | |||
7a44bfa075
|
|||
1c2f25a152
|
|||
be26ea280e
|
|||
b4996d069f
|
|||
bf4892b34d
|
|||
5f84751fd5
|
@@ -14,13 +14,13 @@ The backend of the Solar Network is written in Go and is a microservices app. Th
|
||||
|
||||
## Commit Messages
|
||||
|
||||
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit https://gitmoji.dev
|
||||
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit <https://gitmoji.dev>
|
||||
|
||||
All the commit message should follow `:[gitmoji]: <commit message>` syntax
|
||||
|
||||
## Translations & Localization
|
||||
|
||||
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Crowdin project: https://crowdin.com/project/solian
|
||||
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Crowdin project: <https://crowdin.com/project/solian>
|
||||
|
||||
## New Features
|
||||
|
||||
@@ -28,9 +28,14 @@ To contribute new features, please create an issue or mention the feature you wa
|
||||
|
||||
## Bug Reports / Ask for help
|
||||
|
||||
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
|
||||
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
|
||||
|
||||
## Styles of Code
|
||||
|
||||
Before you create a Pull Request, make sure your code has pass the `flutter analyze` check, if there is any notes, fix as much as possible, if there is no way to fix, do ignore.
|
||||
|
||||
When the code contains comments, use English. We do not any other language of comments existing in the codebase. It might confuse future contributors, cause the code hard to understand and maintaiance.
|
||||
|
||||
-----------
|
||||
|
||||
We appreciate every single commit you contributed. Let's work together and create a better Solar Network!
|
||||
|
||||
|
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
@@ -122,10 +122,6 @@
|
||||
"addVideo": "添加視頻",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上傳文件",
|
||||
"settingsDefaultPool": "選擇文件池",
|
||||
"settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池",
|
||||
|
||||
"createDirectMessage": "創建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"react": "反應",
|
||||
@@ -307,8 +303,7 @@
|
||||
"notifications": "通知",
|
||||
"posts": "帖子",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||
"settingsBackgroundImageClear": "清除背景圖片",
|
||||
"settingsBackgroundImageClear": "清除背景圖片",
|
||||
"settingsBackgroundGenerateColor": "從背景圖像生成主題色",
|
||||
"messageNone": "沒有內容可顯示",
|
||||
"unreadMessages": {
|
||||
@@ -319,8 +314,6 @@
|
||||
"settingsRealmCompactView": "緊湊領域視圖",
|
||||
"settingsMixedFeed": "混合動態",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"settingsHideBottomNav": "隱藏底部導航",
|
||||
"settingsSoundEffects": "音效",
|
||||
"settingsAprilFoolFeatures": "愚人節功能",
|
||||
@@ -675,7 +668,6 @@
|
||||
"publisherFeatureDevelopDescription": "為你的開發者解鎖包括應用套件,API 及更多開發功能。",
|
||||
"publisherFeatureDevelopHint": "目前該功能還在開發中,你需要邀請才可解鎖。",
|
||||
"learnMore": "瞭解更多",
|
||||
"discoverWebArticles": "來自站外的文章",
|
||||
"webArticlesStand": "文章亭",
|
||||
"about": "關於",
|
||||
"somethingWentWrong": "發生了一些錯誤",
|
||||
@@ -698,8 +690,6 @@
|
||||
"sharePostPhoto": "通過圖片分享帖子",
|
||||
"wouldYouLikeToNavigateToChat": "你想要前往聊天頁面嗎?",
|
||||
"abuseReports": "舉報",
|
||||
"discoverRealms": "發現領域",
|
||||
"discoverPublishers": "發現發佈者",
|
||||
"membershipCancel": "取消會員訂閱",
|
||||
"membershipCancelConfirm": "你確定要取消會員訂閱嗎?",
|
||||
"membershipCancelHint": "你確定要取消會員訂閱嗎?你將不會再次被扣費。你的會員資格將在當前計費週期結束前保持有效。並且你將無法重新訂閱,直到當前訂閱結束。",
|
||||
@@ -763,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": "最近投票",
|
||||
@@ -819,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": "找不到應用程式。",
|
||||
@@ -830,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"
|
||||
}
|
@@ -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
|
||||
@@ -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
|
||||
|
@@ -33,17 +33,27 @@ class AppDatabase extends _$AppDatabase {
|
||||
await _migrateToVersion6(m);
|
||||
}
|
||||
if (from < 7) {
|
||||
// Add new columns from SnChatMessage
|
||||
await m.addColumn(chatMessages, chatMessages.updatedAt);
|
||||
await m.addColumn(chatMessages, chatMessages.deletedAt);
|
||||
await m.addColumn(chatMessages, chatMessages.type);
|
||||
await m.addColumn(chatMessages, chatMessages.meta);
|
||||
await m.addColumn(chatMessages, chatMessages.membersMentioned);
|
||||
await m.addColumn(chatMessages, chatMessages.editedAt);
|
||||
await m.addColumn(chatMessages, chatMessages.attachments);
|
||||
await m.addColumn(chatMessages, chatMessages.reactions);
|
||||
await m.addColumn(chatMessages, chatMessages.repliedMessageId);
|
||||
await m.addColumn(chatMessages, chatMessages.forwardedMessageId);
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@@ -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}",
|
||||
);
|
||||
@@ -165,6 +176,21 @@ class IslandApp extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themeProvider);
|
||||
final settings = ref.watch(appSettingsNotifierProvider);
|
||||
|
||||
// Convert string theme mode to ThemeMode enum
|
||||
ThemeMode getThemeMode() {
|
||||
final themeMode = settings.themeMode ?? 'system';
|
||||
switch (themeMode) {
|
||||
case 'light':
|
||||
return ThemeMode.light;
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
case 'system':
|
||||
default:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
void handleMessage(RemoteMessage notification) {
|
||||
if (notification.data['meta']?['action_uri'] != null) {
|
||||
@@ -235,9 +261,10 @@ class IslandApp extends HookConsumerWidget {
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
color: Colors.transparent,
|
||||
theme: theme?.light,
|
||||
darkTheme: theme?.dark,
|
||||
themeMode: ThemeMode.system,
|
||||
themeMode: getThemeMode(),
|
||||
routerConfig: router,
|
||||
supportedLocales: context.supportedLocales,
|
||||
scrollBehavior: AppScrollBehavior(),
|
||||
|
@@ -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(),
|
||||
|
@@ -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,
|
||||
|
@@ -23,31 +23,3 @@ sealed class SnFilePool with _$SnFilePool {
|
||||
factory SnFilePool.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFilePoolFromJson(json);
|
||||
}
|
||||
|
||||
extension SnFilePoolList on List<SnFilePool> {
|
||||
static List<SnFilePool> listFromResponse(dynamic data) {
|
||||
if (data is List) {
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(SnFilePool.fromJson)
|
||||
.toList();
|
||||
}
|
||||
throw ArgumentError('Unexpected response format: $data');
|
||||
}
|
||||
|
||||
List<SnFilePool> filterValid() {
|
||||
return where((p) {
|
||||
final accept = p.policyConfig?['accept_types'];
|
||||
|
||||
if (accept is List) {
|
||||
final acceptsOnlyMedia = accept.every((t) =>
|
||||
t is String &&
|
||||
(t.startsWith('image/') ||
|
||||
t.startsWith('video/') ||
|
||||
t.startsWith('audio/')));
|
||||
if (acceptsOnlyMedia) return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,9 @@ 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';
|
||||
@@ -390,17 +392,35 @@ final rpcServerStateProvider =
|
||||
'message': (socket, dynamic data) async {
|
||||
if (data['cmd'] == 'SET_ACTIVITY') {
|
||||
notifier.addActivity(
|
||||
'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
|
||||
'Activity: ${data['args']['activity']['details'] ?? ''}',
|
||||
);
|
||||
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,
|
||||
data['args']['activity'],
|
||||
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',
|
||||
@@ -420,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',
|
||||
|
50
lib/pods/chat/chat_online_count.dart
Normal file
50
lib/pods/chat/chat_online_count.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'dart:async';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
|
||||
part 'chat_online_count.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatOnlineCountNotifier extends _$ChatOnlineCountNotifier {
|
||||
@override
|
||||
Future<int> build(String chatroomId) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final ws = ref.watch(websocketProvider);
|
||||
|
||||
// Fetch initial online count
|
||||
final response = await apiClient.get(
|
||||
'/sphere/chat/$chatroomId/members/online',
|
||||
);
|
||||
final initialCount = response.data as int;
|
||||
|
||||
// Listen for websocket status updates
|
||||
final subscription = ws.dataStream.listen((WebSocketPacket packet) {
|
||||
if (packet.type == 'accounts.status.update') {
|
||||
final data = packet.data;
|
||||
if (data != null && data['chat_room_id'] == chatroomId) {
|
||||
final status = SnAccountStatus.fromJson(data['status']);
|
||||
var delta = status.isOnline ? 1 : -1;
|
||||
if (status.clearedAt != null &&
|
||||
status.clearedAt!.isBefore(DateTime.now())) {
|
||||
if (status.isInvisible) delta = 1;
|
||||
}
|
||||
// Update count based on online status
|
||||
state.whenData((currentCount) {
|
||||
final newCount = currentCount + delta;
|
||||
state = AsyncData(
|
||||
newCount.clamp(0, double.infinity).toInt(),
|
||||
); // Ensure non-negative
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return initialCount;
|
||||
}
|
||||
}
|
168
lib/pods/chat/chat_online_count.g.dart
Normal file
168
lib/pods/chat/chat_online_count.g.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'chat_online_count.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatOnlineCountNotifierHash() =>
|
||||
r'254ed141ffd99585d898203b3d2b86c4d18db80d';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$ChatOnlineCountNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<int> {
|
||||
late final String chatroomId;
|
||||
|
||||
FutureOr<int> build(String chatroomId);
|
||||
}
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
@ProviderFor(ChatOnlineCountNotifier)
|
||||
const chatOnlineCountNotifierProvider = ChatOnlineCountNotifierFamily();
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
class ChatOnlineCountNotifierFamily extends Family<AsyncValue<int>> {
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
const ChatOnlineCountNotifierFamily();
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
ChatOnlineCountNotifierProvider call(String chatroomId) {
|
||||
return ChatOnlineCountNotifierProvider(chatroomId);
|
||||
}
|
||||
|
||||
@override
|
||||
ChatOnlineCountNotifierProvider getProviderOverride(
|
||||
covariant ChatOnlineCountNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.chatroomId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'chatOnlineCountNotifierProvider';
|
||||
}
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
class ChatOnlineCountNotifierProvider
|
||||
extends AutoDisposeAsyncNotifierProviderImpl<ChatOnlineCountNotifier, int> {
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
ChatOnlineCountNotifierProvider(String chatroomId)
|
||||
: this._internal(
|
||||
() => ChatOnlineCountNotifier()..chatroomId = chatroomId,
|
||||
from: chatOnlineCountNotifierProvider,
|
||||
name: r'chatOnlineCountNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatOnlineCountNotifierHash,
|
||||
dependencies: ChatOnlineCountNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ChatOnlineCountNotifierFamily._allTransitiveDependencies,
|
||||
chatroomId: chatroomId,
|
||||
);
|
||||
|
||||
ChatOnlineCountNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.chatroomId,
|
||||
}) : super.internal();
|
||||
|
||||
final String chatroomId;
|
||||
|
||||
@override
|
||||
FutureOr<int> runNotifierBuild(covariant ChatOnlineCountNotifier notifier) {
|
||||
return notifier.build(chatroomId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ChatOnlineCountNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ChatOnlineCountNotifierProvider._internal(
|
||||
() => create()..chatroomId = chatroomId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
chatroomId: chatroomId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<ChatOnlineCountNotifier, int>
|
||||
createElement() {
|
||||
return _ChatOnlineCountNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ChatOnlineCountNotifierProvider &&
|
||||
other.chatroomId == chatroomId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, chatroomId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ChatOnlineCountNotifierRef on AutoDisposeAsyncNotifierProviderRef<int> {
|
||||
/// The parameter `chatroomId` of this provider.
|
||||
String get chatroomId;
|
||||
}
|
||||
|
||||
class _ChatOnlineCountNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<ChatOnlineCountNotifier, int>
|
||||
with ChatOnlineCountNotifierRef {
|
||||
_ChatOnlineCountNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get chatroomId =>
|
||||
(origin as ChatOnlineCountNotifierProvider).chatroomId;
|
||||
}
|
||||
|
||||
// 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
|
5
lib/pods/chat/chat_rooms.dart
Normal file
5
lib/pods/chat/chat_rooms.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
|
||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
218
lib/pods/chat/chat_subscribe.dart
Normal file
218
lib/pods/chat/chat_subscribe.dart
Normal file
@@ -0,0 +1,218 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/pods/lifecycle.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
import "package:island/pods/websocket.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/widgets/chat/call_button.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
|
||||
part 'chat_subscribe.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
||||
late final String _roomId;
|
||||
late final SnChatRoom _chatRoom;
|
||||
late final SnChatMember _chatIdentity;
|
||||
late final MessagesNotifier _messagesNotifier;
|
||||
|
||||
final List<SnChatMember> _typingStatuses = [];
|
||||
Timer? _typingCleanupTimer;
|
||||
Timer? _typingCooldownTimer;
|
||||
Timer? _periodicSubscribeTimer;
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
@override
|
||||
List<SnChatMember> build(String roomId) {
|
||||
_roomId = roomId;
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final chatRoomAsync = ref.watch(chatroomProvider(roomId));
|
||||
final chatIdentityAsync = ref.watch(chatroomIdentityProvider(roomId));
|
||||
_messagesNotifier = ref.watch(messagesNotifierProvider(roomId).notifier);
|
||||
|
||||
if (chatRoomAsync.isLoading || chatIdentityAsync.isLoading) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (chatRoomAsync.value == null || chatIdentityAsync.value == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
_chatRoom = chatRoomAsync.value!;
|
||||
_chatIdentity = chatIdentityAsync.value!;
|
||||
|
||||
// Subscribe to messages
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Send initial read receipt
|
||||
sendReadReceipt();
|
||||
|
||||
// Set up WebSocket listener
|
||||
_wsSubscription = ws.dataStream.listen(onMessage);
|
||||
|
||||
// Set up typing status cleanup timer
|
||||
_typingCleanupTimer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||
if (_typingStatuses.isNotEmpty) {
|
||||
// Remove typing statuses older than 5 seconds
|
||||
final now = DateTime.now();
|
||||
_typingStatuses.removeWhere((member) {
|
||||
final lastTyped =
|
||||
member.lastTyped ??
|
||||
DateTime.now().subtract(const Duration(milliseconds: 1350));
|
||||
return now.difference(lastTyped).inSeconds > 5;
|
||||
});
|
||||
state = List.of(_typingStatuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up periodic subscribe timer (every 5 minutes)
|
||||
_periodicSubscribeTimer = Timer.periodic(const Duration(minutes: 5), (_) {
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Listen to app lifecycle changes
|
||||
ref.listen(appLifecycleStateProvider, (previous, next) {
|
||||
final lifecycleState = next.value;
|
||||
if (lifecycleState == AppLifecycleState.paused ||
|
||||
lifecycleState == AppLifecycleState.inactive) {
|
||||
// Unsubscribe when app goes to background
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (lifecycleState == AppLifecycleState.resumed) {
|
||||
// Resubscribe when app comes back to foreground
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on dispose
|
||||
ref.onDispose(() {
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
),
|
||||
),
|
||||
);
|
||||
_wsSubscription?.cancel();
|
||||
_typingCleanupTimer?.cancel();
|
||||
_typingCooldownTimer?.cancel();
|
||||
_periodicSubscribeTimer?.cancel();
|
||||
});
|
||||
|
||||
return _typingStatuses;
|
||||
}
|
||||
|
||||
void onMessage(WebSocketPacket pkt) {
|
||||
if (!pkt.type.startsWith('messages')) return;
|
||||
if (['messages.read'].contains(pkt.type)) return;
|
||||
|
||||
if (pkt.type == 'messages.typing' && pkt.data?['sender'] != null) {
|
||||
if (pkt.data?['room_id'] != _chatRoom.id) return;
|
||||
if (pkt.data?['sender_id'] == _chatIdentity.id) return;
|
||||
|
||||
final sender = SnChatMember.fromJson(
|
||||
pkt.data?['sender'],
|
||||
).copyWith(lastTyped: DateTime.now());
|
||||
|
||||
// Check if the sender is already in the typing list
|
||||
final existingIndex = _typingStatuses.indexWhere(
|
||||
(member) => member.id == sender.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
// Update the existing entry with new timestamp
|
||||
_typingStatuses[existingIndex] = sender;
|
||||
} else {
|
||||
// Add new typing status
|
||||
_typingStatuses.add(sender);
|
||||
}
|
||||
state = List.of(_typingStatuses);
|
||||
return;
|
||||
}
|
||||
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
if (message.chatRoomId != _chatRoom.id) return;
|
||||
switch (pkt.type) {
|
||||
case 'messages.new':
|
||||
case 'messages.update':
|
||||
case 'messages.delete':
|
||||
if (message.type.startsWith('call')) {
|
||||
// Handle the ongoing call.
|
||||
ref.invalidate(ongoingCallProvider(message.chatRoomId));
|
||||
}
|
||||
_messagesNotifier.receiveMessage(message);
|
||||
// Send read receipt for new message
|
||||
sendReadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
void sendReadReceipt() {
|
||||
// Send websocket packet
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.read',
|
||||
data: {'chat_room_id': _roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void sendTypingStatus() {
|
||||
// Don't send if we're already in a cooldown period
|
||||
if (_typingCooldownTimer != null) return;
|
||||
|
||||
// Send typing status immediately
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.typing',
|
||||
data: {'chat_room_id': _roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_typingCooldownTimer = Timer(const Duration(milliseconds: 850), () {
|
||||
_typingCooldownTimer = null;
|
||||
});
|
||||
}
|
||||
}
|
176
lib/pods/chat/chat_subscribe.g.dart
Normal file
176
lib/pods/chat/chat_subscribe.g.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'chat_subscribe.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatSubscribeNotifierHash() =>
|
||||
r'10a6b2c687149ebb419e4c96349d8bab1f183ec6';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$ChatSubscribeNotifier
|
||||
extends BuildlessAutoDisposeNotifier<List<SnChatMember>> {
|
||||
late final String roomId;
|
||||
|
||||
List<SnChatMember> build(String roomId);
|
||||
}
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
@ProviderFor(ChatSubscribeNotifier)
|
||||
const chatSubscribeNotifierProvider = ChatSubscribeNotifierFamily();
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
class ChatSubscribeNotifierFamily extends Family<List<SnChatMember>> {
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
const ChatSubscribeNotifierFamily();
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
ChatSubscribeNotifierProvider call(String roomId) {
|
||||
return ChatSubscribeNotifierProvider(roomId);
|
||||
}
|
||||
|
||||
@override
|
||||
ChatSubscribeNotifierProvider getProviderOverride(
|
||||
covariant ChatSubscribeNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.roomId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'chatSubscribeNotifierProvider';
|
||||
}
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
class ChatSubscribeNotifierProvider
|
||||
extends
|
||||
AutoDisposeNotifierProviderImpl<
|
||||
ChatSubscribeNotifier,
|
||||
List<SnChatMember>
|
||||
> {
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
ChatSubscribeNotifierProvider(String roomId)
|
||||
: this._internal(
|
||||
() => ChatSubscribeNotifier()..roomId = roomId,
|
||||
from: chatSubscribeNotifierProvider,
|
||||
name: r'chatSubscribeNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatSubscribeNotifierHash,
|
||||
dependencies: ChatSubscribeNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ChatSubscribeNotifierFamily._allTransitiveDependencies,
|
||||
roomId: roomId,
|
||||
);
|
||||
|
||||
ChatSubscribeNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.roomId,
|
||||
}) : super.internal();
|
||||
|
||||
final String roomId;
|
||||
|
||||
@override
|
||||
List<SnChatMember> runNotifierBuild(
|
||||
covariant ChatSubscribeNotifier notifier,
|
||||
) {
|
||||
return notifier.build(roomId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ChatSubscribeNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ChatSubscribeNotifierProvider._internal(
|
||||
() => create()..roomId = roomId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
roomId: roomId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<ChatSubscribeNotifier, List<SnChatMember>>
|
||||
createElement() {
|
||||
return _ChatSubscribeNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ChatSubscribeNotifierProvider && other.roomId == roomId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, roomId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ChatSubscribeNotifierRef
|
||||
on AutoDisposeNotifierProviderRef<List<SnChatMember>> {
|
||||
/// The parameter `roomId` of this provider.
|
||||
String get roomId;
|
||||
}
|
||||
|
||||
class _ChatSubscribeNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeNotifierProviderElement<
|
||||
ChatSubscribeNotifier,
|
||||
List<SnChatMember>
|
||||
>
|
||||
with ChatSubscribeNotifierRef {
|
||||
_ChatSubscribeNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get roomId => (origin as ChatSubscribeNotifierProvider).roomId;
|
||||
}
|
||||
|
||||
// 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
|
@@ -1,6 +1,7 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
|
||||
part 'chat_summary.g.dart';
|
||||
|
||||
@@ -12,9 +13,27 @@ class ChatSummary extends _$ChatSummary {
|
||||
final resp = await client.get('/sphere/chat/summary');
|
||||
|
||||
final Map<String, dynamic> data = resp.data;
|
||||
return data.map(
|
||||
final summaries = data.map(
|
||||
(key, value) => MapEntry(key, SnChatSummary.fromJson(value)),
|
||||
);
|
||||
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final subscription = ws.dataStream.listen((WebSocketPacket pkt) {
|
||||
if (!pkt.type.startsWith('messages')) return;
|
||||
if (pkt.type == 'messages.new') {
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
updateLastMessage(message.chatRoomId, message);
|
||||
} else if (pkt.type == 'messages.update') {
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
updateMessageContent(message.chatRoomId, message);
|
||||
}
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
Future<void> clearUnreadCount(String chatId) async {
|
||||
@@ -61,4 +80,19 @@ class ChatSummary extends _$ChatSummary {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void updateMessageContent(String chatId, SnChatMessage message) {
|
||||
state.whenData((summaries) {
|
||||
final summary = summaries[chatId];
|
||||
if (summary != null && summary.lastMessage?.id == message.id) {
|
||||
state = AsyncData({
|
||||
...summaries,
|
||||
chatId: SnChatSummary(
|
||||
unreadCount: summary.unreadCount,
|
||||
lastMessage: message,
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ part of 'chat_summary.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatSummaryHash() => r'87a10e4cefa37dc5fa8eadb175ef1b2bed6070bf';
|
||||
String _$chatSummaryHash() => r'7b79dba7445f634373fbb2ee0ced99b2302097c2';
|
||||
|
||||
/// See also [ChatSummary].
|
||||
@ProviderFor(ChatSummary)
|
@@ -10,13 +10,14 @@ 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/lifecycle.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";
|
||||
import "package:island/pods/chat/chat_rooms.dart";
|
||||
|
||||
part 'messages_notifier.g.dart';
|
||||
|
||||
@@ -39,6 +40,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
bool _hasMore = true;
|
||||
bool _isSyncing = false;
|
||||
bool _isJumping = false;
|
||||
DateTime? _lastPauseTime;
|
||||
|
||||
@override
|
||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||
@@ -66,13 +68,31 @@ class MessagesNotifier extends _$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();
|
||||
}
|
||||
next.whenData((state) {
|
||||
if (state == AppLifecycleState.paused) {
|
||||
_lastPauseTime = DateTime.now();
|
||||
developer.log(
|
||||
'App paused, recording time',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
if (_lastPauseTime != null) {
|
||||
final diff = DateTime.now().difference(_lastPauseTime!);
|
||||
if (diff > const Duration(minutes: 1)) {
|
||||
developer.log(
|
||||
'App resumed after >1 min, syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
syncMessages();
|
||||
} else {
|
||||
developer.log(
|
||||
'App resumed within 1 min, skipping sync',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -613,6 +633,16 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_sortMessages([localMessage, ...currentMessages]),
|
||||
);
|
||||
}
|
||||
|
||||
switch (remoteMessage.type) {
|
||||
case "messages.delete":
|
||||
await receiveMessageDeletion(
|
||||
remoteMessage.meta['message_id'] ?? remoteMessage.id,
|
||||
);
|
||||
case "messages.update":
|
||||
case "messages.update.links":
|
||||
await receiveMessageUpdate(remoteMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
||||
@@ -622,8 +652,12 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
final targetId = remoteMessage.meta['message_id'] ?? remoteMessage.id;
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
remoteMessage.copyWith(
|
||||
id: targetId,
|
||||
meta: Map.of(remoteMessage.meta)..remove('message_id'),
|
||||
),
|
||||
MessageStatus.sent,
|
||||
);
|
||||
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
|
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237';
|
||||
String _$messagesNotifierHash() => r'4257c9b3792418e913d0bac3ef58e727314635af';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
@@ -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,9 +25,11 @@ 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 kAppThemeMode = 'app_theme_mode';
|
||||
const kFeaturedPostsCollapsedId =
|
||||
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
||||
|
||||
@@ -67,8 +70,10 @@ 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,
|
||||
required String? themeMode,
|
||||
}) = _AppSettings;
|
||||
}
|
||||
|
||||
@@ -88,8 +93,10 @@ 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',
|
||||
themeMode: prefs.getString(kAppThemeMode) ?? 'system',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,6 +203,19 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
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));
|
||||
}
|
||||
|
||||
void setThemeMode(String value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setString(kAppThemeMode, value);
|
||||
state = state.copyWith(themeMode: value);
|
||||
}
|
||||
}
|
||||
|
||||
final updateInfoProvider =
|
||||
|
@@ -16,7 +16,8 @@ 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;// The window size for desktop platforms
|
||||
String? get defaultPoolId; String get messageDisplayStyle;
|
||||
double get windowOpacity;// The window opacity for desktop platforms
|
||||
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode;
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -27,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)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
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)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,windowOpacity,defaultPoolId,messageDisplayStyle,themeMode);
|
||||
|
||||
@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, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
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, themeMode: $themeMode)';
|
||||
}
|
||||
|
||||
|
||||
@@ -47,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, String? defaultPoolId, String messageDisplayStyle
|
||||
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, String? themeMode
|
||||
});
|
||||
|
||||
|
||||
@@ -62,9 +63,7 @@ class _$AppSettingsCopyWithImpl<$Res>
|
||||
final AppSettings _self;
|
||||
final $Res Function(AppSettings) _then;
|
||||
|
||||
/// 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,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
@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,Object? themeMode = 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
|
||||
@@ -76,7 +75,8 @@ 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?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
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,
|
||||
));
|
||||
@@ -160,10 +160,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $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,_that.defaultPoolId,_that.messageDisplayStyle);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();
|
||||
|
||||
}
|
||||
@@ -181,10 +181,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle) $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,_that.defaultPoolId,_that.messageDisplayStyle);}
|
||||
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`
|
||||
///
|
||||
@@ -198,10 +198,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $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,_that.defaultPoolId,_that.messageDisplayStyle);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;
|
||||
|
||||
}
|
||||
@@ -213,7 +213,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, required this.defaultPoolId, required this.messageDisplayStyle});
|
||||
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, required this.themeMode});
|
||||
|
||||
|
||||
@override final bool autoTranslate;
|
||||
@@ -228,8 +228,11 @@ class _AppSettings implements AppSettings {
|
||||
// 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;
|
||||
@override final String? themeMode;
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -241,16 +244,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)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
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,defaultPoolId,messageDisplayStyle);
|
||||
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, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
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)';
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +264,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, String? defaultPoolId, String messageDisplayStyle
|
||||
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, String? themeMode
|
||||
});
|
||||
|
||||
|
||||
@@ -278,7 +281,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,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
@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,Object? themeMode = 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
|
||||
@@ -290,8 +293,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?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
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,themeMode: null == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ part of 'config.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$appSettingsNotifierHash() =>
|
||||
r'9f0979f18b107e61185391e7c39bd81ac4b8ca50';
|
||||
r'3b0967a39a375c664c3fd44cbee7936b8b2f5fec';
|
||||
|
||||
/// See also [AppSettingsNotifier].
|
||||
@ProviderFor(AppSettingsNotifier)
|
||||
|
@@ -6,23 +6,19 @@ 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');
|
||||
final pools = SnFilePoolList.listFromResponse(response.data);
|
||||
return pools.filterValid();
|
||||
return response.data
|
||||
.map((e) => SnFilePool.fromJson(e))
|
||||
.cast<SnFilePool>()
|
||||
.toList();
|
||||
});
|
||||
|
||||
String resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) {
|
||||
String? resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) {
|
||||
final settings = ref.watch(appSettingsNotifierProvider);
|
||||
final validPools = pools.filterValid();
|
||||
|
||||
final configuredId = settings.defaultPoolId;
|
||||
if (configuredId != null && validPools.any((p) => p.id == configuredId)) {
|
||||
if (configuredId != null && pools.any((p) => p.id == configuredId)) {
|
||||
return configuredId;
|
||||
}
|
||||
|
||||
if (validPools.isNotEmpty) {
|
||||
return validPools.first.id;
|
||||
}
|
||||
|
||||
// DEFAULT: Solar Network Driver
|
||||
return '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; }
|
||||
|
||||
return pools.firstOrNull?.id;
|
||||
}
|
||||
|
@@ -2,10 +2,6 @@ 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>();
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -211,21 +211,11 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
icon: Symbols.system_update,
|
||||
title: 'Check for updates',
|
||||
onTap: () async {
|
||||
// Fetch latest release and show the unified sheet
|
||||
final svc = UpdateService();
|
||||
// Reuse service fetch + compare to decide content
|
||||
showLoadingModal(context);
|
||||
final release = await svc.fetchLatestRelease();
|
||||
svc.checkForUpdates(context);
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
if (release != null) {
|
||||
await svc.showUpdateSheet(context, release);
|
||||
} else {
|
||||
showInfoAlert(
|
||||
'Currently cannot get update from the GitHub.',
|
||||
'Unable to check for updates',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildListTile(
|
||||
|
@@ -148,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),
|
||||
@@ -180,6 +179,12 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
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,
|
||||
@@ -190,12 +195,6 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'${'levelingProgressLevel'.tr(args: [currentLevel.toString()])} / 120',
|
||||
textAlign: TextAlign.right,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, top: 16, bottom: 12),
|
||||
),
|
||||
@@ -272,17 +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),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMembershipSection(context, ref, stellarSubscription),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart' hide ConnectionState;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/call_button.dart';
|
||||
import 'package:island/widgets/chat/call_overlay.dart';
|
||||
|
@@ -11,8 +11,8 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat_summary.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/chat/chat_summary.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
@@ -178,6 +178,102 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
||||
.toList();
|
||||
}
|
||||
|
||||
class ChatListBodyWidget extends HookConsumerWidget {
|
||||
final bool isFloating;
|
||||
final TabController tabController;
|
||||
final ValueNotifier<int> selectedTab;
|
||||
|
||||
const ChatListBodyWidget({
|
||||
super.key,
|
||||
this.isFloating = false,
|
||||
required this.tabController,
|
||||
required this.selectedTab,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final chats = ref.watch(chatroomsJoinedProvider);
|
||||
final callState = ref.watch(callNotifierProvider);
|
||||
|
||||
Widget bodyWidget = Column(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final summaryState = ref.watch(chatSummaryProvider);
|
||||
return summaryState.maybeWhen(
|
||||
loading:
|
||||
() => const LinearProgressIndicator(
|
||||
minHeight: 2,
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: chats.when(
|
||||
data:
|
||||
(items) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync(() {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
}),
|
||||
child: ListView.builder(
|
||||
padding: getTabbedPadding(
|
||||
context,
|
||||
bottom: callState.isConnected ? 96 : null,
|
||||
),
|
||||
itemCount:
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 && item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems =
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 &&
|
||||
item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.toList();
|
||||
final item = filteredItems[index];
|
||||
return ChatRoomListTile(
|
||||
room: item,
|
||||
isDirect: item.type == 1,
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return isFloating ? Card(child: bodyWidget) : bodyWidget;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatShellScreen extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
const ChatShellScreen({super.key, required this.child});
|
||||
@@ -191,9 +287,23 @@ class ChatShellScreen extends HookConsumerWidget {
|
||||
isRoot: true,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(flex: 2, child: ChatListScreen(isAside: true)),
|
||||
const VerticalDivider(width: 1),
|
||||
Flexible(flex: 4, child: child),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: ChatListScreen(
|
||||
isAside: true,
|
||||
isFloating: true,
|
||||
).padding(left: 16, vertical: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
flex: 4,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
),
|
||||
child: child,
|
||||
).padding(top: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -205,24 +315,23 @@ class ChatShellScreen extends HookConsumerWidget {
|
||||
|
||||
class ChatListScreen extends HookConsumerWidget {
|
||||
final bool isAside;
|
||||
const ChatListScreen({super.key, this.isAside = false});
|
||||
final bool isFloating;
|
||||
const ChatListScreen({
|
||||
super.key,
|
||||
this.isAside = false,
|
||||
this.isFloating = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isWide = isWideScreen(context);
|
||||
if (isWide && !isAside) {
|
||||
return const EmptyPageHolder();
|
||||
}
|
||||
|
||||
final chats = ref.watch(chatroomsJoinedProvider);
|
||||
final chatInvites = ref.watch(chatroomInvitesProvider);
|
||||
final tabController = useTabController(initialLength: 3);
|
||||
final selectedTab = useState(
|
||||
0,
|
||||
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
|
||||
|
||||
final callState = ref.watch(callNotifierProvider);
|
||||
|
||||
useEffect(() {
|
||||
tabController.addListener(() {
|
||||
selectedTab.value = tabController.index;
|
||||
@@ -250,6 +359,76 @@ class ChatListScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
if (isAside) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
controller: tabController,
|
||||
tabAlignment: TabAlignment.start,
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
const Tab(icon: Icon(Symbols.chat)),
|
||||
const Tab(icon: Icon(Symbols.person)),
|
||||
const Tab(icon: Icon(Symbols.group)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
icon: Badge(
|
||||
label: Text(
|
||||
chatInvites.when(
|
||||
data: (invites) => invites.length.toString(),
|
||||
error: (_, _) => '0',
|
||||
loading: () => '0',
|
||||
),
|
||||
),
|
||||
isLabelVisible: chatInvites.when(
|
||||
data: (invites) => invites.isNotEmpty,
|
||||
error: (_, _) => false,
|
||||
loading: () => false,
|
||||
),
|
||||
child: const Icon(Symbols.email),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const _ChatInvitesSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ChatListBodyWidget(
|
||||
isFloating: false,
|
||||
tabController: tabController,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isWide && !isAside) {
|
||||
return const EmptyPageHolder();
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
extendBody: false, // Prevent conflicts with tabs navigation
|
||||
appBar: AppBar(
|
||||
@@ -353,81 +532,10 @@ class ChatListScreen extends HookConsumerWidget {
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
floatingActionButtonLocation: TabbedFabLocation(context),
|
||||
body: Column(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final summaryState = ref.watch(chatSummaryProvider);
|
||||
return summaryState.maybeWhen(
|
||||
loading:
|
||||
() => const LinearProgressIndicator(
|
||||
minHeight: 2,
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: chats.when(
|
||||
data:
|
||||
(items) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync(() {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
}),
|
||||
child: ListView.builder(
|
||||
padding: getTabbedPadding(
|
||||
context,
|
||||
bottom: callState.isConnected ? 96 : null,
|
||||
),
|
||||
itemCount:
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 &&
|
||||
item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems =
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 &&
|
||||
item.type == 1) ||
|
||||
(selectedTab.value == 2 &&
|
||||
item.type != 1),
|
||||
)
|
||||
.toList();
|
||||
final item = filteredItems[index];
|
||||
return ChatRoomListTile(
|
||||
room: item,
|
||||
isDirect: item.type == 1,
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: ChatListBodyWidget(
|
||||
isFloating: false,
|
||||
tabController: tabController,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ 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";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
|
||||
class PublicRoomPreview extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "dart:typed_data";
|
||||
import "package:cross_file/cross_file.dart";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:file_picker/file_picker.dart";
|
||||
import "package:flutter/material.dart";
|
||||
@@ -12,12 +9,12 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/models/file_pool.dart";
|
||||
import "package:island/pods/chat/chat_rooms.dart";
|
||||
import "package:island/pods/chat/chat_subscribe.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/pods/file_pool.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/pods/websocket.dart";
|
||||
import "package:island/pods/chat/chat_online_count.dart";
|
||||
import "package:island/services/file.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
@@ -26,9 +23,7 @@ import "package:island/widgets/app_scaffold.dart";
|
||||
import "package:island/widgets/attachment_uploader.dart";
|
||||
import "package:island/widgets/chat/call_overlay.dart";
|
||||
import "package:island/widgets/chat/message_item.dart";
|
||||
import "package:island/widgets/content/attachment_preview.dart";
|
||||
import "package:island/widgets/content/cloud_files.dart";
|
||||
import "package:island/widgets/content/sheet.dart";
|
||||
import "package:island/widgets/post/compose_shared.dart";
|
||||
import "package:island/widgets/response.dart";
|
||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||
@@ -39,37 +34,6 @@ import "package:island/widgets/chat/call_button.dart";
|
||||
import "package:island/widgets/chat/chat_input.dart";
|
||||
import "package:island/widgets/chat/public_room_preview.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);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatRoomScreen extends HookConsumerWidget {
|
||||
final String id;
|
||||
const ChatRoomScreen({super.key, required this.id});
|
||||
@@ -79,6 +43,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final chatRoom = ref.watch(chatroomProvider(id));
|
||||
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
|
||||
final isSyncing = ref.watch(isSyncingProvider);
|
||||
final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id));
|
||||
|
||||
final hasOnlineCount = onlineCount.hasValue;
|
||||
|
||||
if (chatIdentity.isLoading || chatRoom.isLoading) {
|
||||
return AppScaffold(
|
||||
@@ -163,7 +130,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
|
||||
final messages = ref.watch(messagesNotifierProvider(id));
|
||||
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final chatSubscribeNotifier = ref.read(
|
||||
chatSubscribeNotifierProvider(id).notifier,
|
||||
);
|
||||
|
||||
final messageController = useTextEditingController();
|
||||
final scrollController = useScrollController();
|
||||
@@ -174,65 +143,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final attachments = useState<List<UniversalFile>>([]);
|
||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
||||
|
||||
// Function to send read receipt
|
||||
void sendReadReceipt() async {
|
||||
// Send websocket packet
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.read',
|
||||
data: {'chat_room_id': id},
|
||||
endpoint: 'DysonNetwork.Sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Members who are typing
|
||||
final typingStatuses = useState<List<SnChatMember>>([]);
|
||||
final typingDebouncer = useState<Timer?>(null);
|
||||
|
||||
void sendTypingStatus() {
|
||||
// Don't send if we're already in a cooldown period
|
||||
if (typingDebouncer.value != null) return;
|
||||
|
||||
// Send typing status immediately
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.typing',
|
||||
data: {'chat_room_id': id},
|
||||
endpoint: 'DysonNetwork.Sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
typingDebouncer.value = Timer(const Duration(milliseconds: 850), () {
|
||||
typingDebouncer.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Add timer to remove typing status after inactivity
|
||||
useEffect(() {
|
||||
final removeTypingTimer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||
if (typingStatuses.value.isNotEmpty) {
|
||||
// Remove typing statuses older than 5 seconds
|
||||
final now = DateTime.now();
|
||||
typingStatuses.value =
|
||||
typingStatuses.value.where((member) {
|
||||
final lastTyped =
|
||||
member.lastTyped ??
|
||||
DateTime.now().subtract(const Duration(milliseconds: 1350));
|
||||
return now.difference(lastTyped).inSeconds < 5;
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
|
||||
return () => removeTypingTimer.cancel();
|
||||
}, []);
|
||||
|
||||
var isLoading = false;
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
@@ -252,85 +162,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
// Add websocket listener for new messages
|
||||
useEffect(() {
|
||||
void onMessage(WebSocketPacket pkt) {
|
||||
if (!pkt.type.startsWith('messages')) return;
|
||||
if (['messages.read'].contains(pkt.type)) return;
|
||||
|
||||
if (pkt.type == 'messages.typing' && pkt.data?['sender'] != null) {
|
||||
if (pkt.data?['room_id'] != chatRoom.value?.id) return;
|
||||
if (pkt.data?['sender_id'] == chatIdentity.value?.id) return;
|
||||
|
||||
final sender = SnChatMember.fromJson(
|
||||
pkt.data?['sender'],
|
||||
).copyWith(lastTyped: DateTime.now());
|
||||
|
||||
// Check if the sender is already in the typing list
|
||||
final existingIndex = typingStatuses.value.indexWhere(
|
||||
(member) => member.id == sender.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
// Update the existing entry with new timestamp
|
||||
final updatedList = [...typingStatuses.value];
|
||||
updatedList[existingIndex] = sender;
|
||||
typingStatuses.value = updatedList;
|
||||
} else {
|
||||
// Add new typing status
|
||||
typingStatuses.value = [...typingStatuses.value, sender];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
if (message.chatRoomId != chatRoom.value?.id) return;
|
||||
switch (pkt.type) {
|
||||
case 'messages.new':
|
||||
if (message.type.startsWith('call')) {
|
||||
// Handle the ongoing call.
|
||||
ref.invalidate(ongoingCallProvider(message.chatRoomId));
|
||||
}
|
||||
messagesNotifier.receiveMessage(message);
|
||||
// Send read receipt for new message
|
||||
sendReadReceipt();
|
||||
case 'messages.update':
|
||||
messagesNotifier.receiveMessageUpdate(message).then((_) {
|
||||
messagesNotifier.receiveMessage(message);
|
||||
});
|
||||
case 'messages.delete':
|
||||
messagesNotifier.receiveMessageDeletion(message.id).then((_) {
|
||||
messagesNotifier.receiveMessage(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendReadReceipt();
|
||||
final subscription = ws.dataStream.listen(onMessage);
|
||||
return () => subscription.cancel();
|
||||
}, [ws, chatRoom]);
|
||||
|
||||
useEffect(() {
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': id},
|
||||
),
|
||||
),
|
||||
);
|
||||
return () {
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': id},
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
Future<void> pickPhotoMedia() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
@@ -364,21 +195,19 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
void sendMessage() {
|
||||
if (messageController.text.trim().isNotEmpty ||
|
||||
attachments.value.isNotEmpty) {
|
||||
messagesNotifier
|
||||
.sendMessage(
|
||||
messageController.text.trim(),
|
||||
attachments.value,
|
||||
editingTo: messageEditingTo.value,
|
||||
forwardingTo: messageForwardingTo.value,
|
||||
replyingTo: messageReplyingTo.value,
|
||||
onProgress: (messageId, progress) {
|
||||
attachmentProgress.value = {
|
||||
...attachmentProgress.value,
|
||||
messageId: progress,
|
||||
};
|
||||
},
|
||||
)
|
||||
.then((_) => sendReadReceipt());
|
||||
messagesNotifier.sendMessage(
|
||||
messageController.text.trim(),
|
||||
attachments.value,
|
||||
editingTo: messageEditingTo.value,
|
||||
forwardingTo: messageForwardingTo.value,
|
||||
replyingTo: messageReplyingTo.value,
|
||||
onProgress: (messageId, progress) {
|
||||
attachmentProgress.value = {
|
||||
...attachmentProgress.value,
|
||||
messageId: progress,
|
||||
};
|
||||
},
|
||||
);
|
||||
messageController.clear();
|
||||
messageEditingTo.value = null;
|
||||
messageReplyingTo.value = null;
|
||||
@@ -391,7 +220,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
useEffect(() {
|
||||
void onTextChange() {
|
||||
if (messageController.text.isNotEmpty) {
|
||||
sendTypingStatus();
|
||||
chatSubscribeNotifier.sendTypingStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,6 +230,32 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
|
||||
final compactHeader = isWideScreen(context);
|
||||
|
||||
Widget onlineIndicator() => Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: (onlineCount as AsyncData).value > 1 ? Colors.green : null,
|
||||
border:
|
||||
(onlineCount as AsyncData).value <= 1
|
||||
? Border.all(color: Colors.grey)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(onlineCount as AsyncData).value} online',
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget comfortHeaderWidget(SnChatRoom? room) => Column(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -434,16 +289,18 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(15),
|
||||
if (hasOnlineCount) onlineIndicator(),
|
||||
],
|
||||
);
|
||||
|
||||
Widget compactHeaderWidget(SnChatRoom? room) => Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
height: 28,
|
||||
width: 28,
|
||||
child:
|
||||
(room!.type == 1 && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
@@ -469,6 +326,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(19),
|
||||
if (hasOnlineCount)
|
||||
onlineIndicator().padding(left: 6),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -482,7 +341,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => ChatAttachmentUploaderSheet(
|
||||
(context) => AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
attachments: attachments.value,
|
||||
index: index,
|
||||
@@ -541,7 +400,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||
SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom: 80 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
@@ -655,7 +517,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
||||
automaticallyImplyLeading: false,
|
||||
toolbarHeight: compactHeader ? null : 64,
|
||||
toolbarHeight: compactHeader ? null : 80,
|
||||
title: chatRoom.when(
|
||||
data:
|
||||
(room) =>
|
||||
@@ -723,169 +585,92 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom:
|
||||
isSyncing
|
||||
? const PreferredSize(
|
||||
preferredSize: Size.fromHeight(2),
|
||||
child: LinearProgressIndicator(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
chatRoom.when(
|
||||
data:
|
||||
(room) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, -0.3),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1.0,
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
// Messages
|
||||
Positioned.fill(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Input
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: chatRoom.when(
|
||||
data:
|
||||
(room) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ChatInput(
|
||||
messageController: messageController,
|
||||
chatRoom: room!,
|
||||
onSend: sendMessage,
|
||||
onClear: () {
|
||||
if (messageEditingTo.value != null) {
|
||||
attachments.value.clear();
|
||||
messageController.clear();
|
||||
}
|
||||
messageEditingTo.value = null;
|
||||
messageReplyingTo.value = null;
|
||||
messageForwardingTo.value = null;
|
||||
},
|
||||
messageEditingTo: messageEditingTo.value,
|
||||
messageReplyingTo: messageReplyingTo.value,
|
||||
messageForwardingTo: messageForwardingTo.value,
|
||||
onPickFile: (bool isPhoto) {
|
||||
if (isPhoto) {
|
||||
pickPhotoMedia();
|
||||
} else {
|
||||
pickVideoMedia();
|
||||
}
|
||||
},
|
||||
attachments: attachments.value,
|
||||
onUploadAttachment: uploadAttachment,
|
||||
onDeleteAttachment: (index) async {
|
||||
final attachment = attachments.value[index];
|
||||
if (attachment.isOnCloud) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/files/${attachment.data.id}',
|
||||
);
|
||||
},
|
||||
child:
|
||||
typingStatuses.value.isNotEmpty
|
||||
? Container(
|
||||
key: const ValueKey('typing-indicator'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.more_horiz,
|
||||
size: 16,
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'typingHint'.plural(
|
||||
typingStatuses.value.length,
|
||||
args: [
|
||||
typingStatuses.value
|
||||
.map(
|
||||
(x) =>
|
||||
x.nick ??
|
||||
x.account.nick,
|
||||
)
|
||||
.join(', '),
|
||||
],
|
||||
),
|
||||
style:
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('typing-indicator-none'),
|
||||
),
|
||||
),
|
||||
ChatInput(
|
||||
messageController: messageController,
|
||||
chatRoom: room!,
|
||||
onSend: sendMessage,
|
||||
onClear: () {
|
||||
if (messageEditingTo.value != null) {
|
||||
attachments.value.clear();
|
||||
messageController.clear();
|
||||
}
|
||||
messageEditingTo.value = null;
|
||||
messageReplyingTo.value = null;
|
||||
messageForwardingTo.value = null;
|
||||
},
|
||||
messageEditingTo: messageEditingTo.value,
|
||||
messageReplyingTo: messageReplyingTo.value,
|
||||
messageForwardingTo: messageForwardingTo.value,
|
||||
onPickFile: (bool isPhoto) {
|
||||
if (isPhoto) {
|
||||
pickPhotoMedia();
|
||||
} else {
|
||||
pickVideoMedia();
|
||||
}
|
||||
},
|
||||
attachments: attachments.value,
|
||||
onUploadAttachment: uploadAttachment,
|
||||
onDeleteAttachment: (index) async {
|
||||
final attachment = attachments.value[index];
|
||||
if (attachment.isOnCloud) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/files/${attachment.data.id}',
|
||||
);
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.removeAt(index);
|
||||
attachments.value = clone;
|
||||
},
|
||||
onMoveAttachment: (idx, delta) {
|
||||
if (idx + delta < 0 ||
|
||||
idx + delta >= attachments.value.length) {
|
||||
return;
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.insert(idx + delta, clone.removeAt(idx));
|
||||
attachments.value = clone;
|
||||
},
|
||||
onAttachmentsChanged: (newAttachments) {
|
||||
attachments.value = newAttachments;
|
||||
},
|
||||
attachmentProgress: attachmentProgress.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.removeAt(index);
|
||||
attachments.value = clone;
|
||||
},
|
||||
onMoveAttachment: (idx, delta) {
|
||||
if (idx + delta < 0 ||
|
||||
idx + delta >= attachments.value.length) {
|
||||
return;
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.insert(idx + delta, clone.removeAt(idx));
|
||||
attachments.value = clone;
|
||||
},
|
||||
onAttachmentsChanged: (newAttachments) {
|
||||
attachments.value = newAttachments;
|
||||
},
|
||||
attachmentProgress: attachmentProgress.value,
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
@@ -893,349 +678,37 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
top: 0,
|
||||
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
||||
),
|
||||
if (isSyncing)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).scaffoldBackgroundColor.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Syncing...',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatAttachmentUploaderSheet extends StatefulWidget {
|
||||
final WidgetRef ref;
|
||||
final List<UniversalFile> attachments;
|
||||
final int index;
|
||||
|
||||
const ChatAttachmentUploaderSheet({
|
||||
super.key,
|
||||
required this.ref,
|
||||
required this.attachments,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatAttachmentUploaderSheet> createState() =>
|
||||
_ChatAttachmentUploaderSheetState();
|
||||
}
|
||||
|
||||
class _ChatAttachmentUploaderSheetState
|
||||
extends State<ChatAttachmentUploaderSheet> {
|
||||
String? selectedPoolId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final attachment = widget.attachments[widget.index];
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'uploadAttachment'.tr(),
|
||||
child: FutureBuilder<List<SnFilePool>>(
|
||||
future: widget.ref.read(poolsProvider.future),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('errorLoadingPools'.tr()));
|
||||
}
|
||||
final pools = snapshot.data!.filterValid();
|
||||
selectedPoolId ??= resolveDefaultPoolId(widget.ref, pools);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedPoolId,
|
||||
items:
|
||||
pools.map((pool) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPoolId = value;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'selectPool'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'choosePool'.tr(),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
FutureBuilder<int?>(
|
||||
future: _getFileSize(attachment),
|
||||
builder: (context, sizeSnapshot) {
|
||||
if (!sizeSnapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final fileSize = sizeSnapshot.data!;
|
||||
final selectedPool = pools.firstWhere(
|
||||
(p) => p.id == selectedPoolId,
|
||||
);
|
||||
|
||||
// Check file size limit
|
||||
final maxFileSize =
|
||||
selectedPool.policyConfig?['max_file_size']
|
||||
as int?;
|
||||
final fileSizeExceeded =
|
||||
maxFileSize != null && fileSize > maxFileSize;
|
||||
|
||||
// Check accepted types
|
||||
final acceptTypes =
|
||||
selectedPool.policyConfig?['accept_types']
|
||||
as List?;
|
||||
final mimeType =
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(
|
||||
attachment.type,
|
||||
);
|
||||
final typeAccepted =
|
||||
acceptTypes == null ||
|
||||
acceptTypes.isEmpty ||
|
||||
acceptTypes.any(
|
||||
(type) => mimeType.startsWith(type),
|
||||
);
|
||||
|
||||
final hasIssues = fileSizeExceeded || !typeAccepted;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasIssues) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.warning,
|
||||
size: 18,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'uploadConstraints'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (fileSizeExceeded) ...[
|
||||
const Gap(4),
|
||||
Text(
|
||||
'fileSizeExceeded'.tr(
|
||||
args: [
|
||||
_formatFileSize(maxFileSize),
|
||||
],
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!typeAccepted) ...[
|
||||
const Gap(4),
|
||||
Text(
|
||||
'fileTypeNotAccepted'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
],
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.account_balance_wallet,
|
||||
size: 18,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'quotaCostInfo'.tr(
|
||||
args: [
|
||||
_formatQuotaCost(
|
||||
fileSize,
|
||||
selectedPool,
|
||||
),
|
||||
],
|
||||
),
|
||||
style:
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium,
|
||||
).fontSize(13),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 4),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(4),
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(Symbols.info, size: 18),
|
||||
Text(
|
||||
'attachmentPreview'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).fontSize(13),
|
||||
],
|
||||
).padding(horizontal: 4),
|
||||
const Gap(8),
|
||||
AttachmentPreview(item: attachment, isCompact: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Symbols.close),
|
||||
label: Text('cancel').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
TextButton.icon(
|
||||
onPressed: () => _confirmUpload(),
|
||||
icon: const Icon(Symbols.upload),
|
||||
label: Text('upload').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<AttachmentUploadConfig?> _getUploadConfig() async {
|
||||
final attachment = widget.attachments[widget.index];
|
||||
final fileSize = await _getFileSize(attachment);
|
||||
|
||||
if (fileSize == null) return null;
|
||||
|
||||
// Get the selected pool to check constraints
|
||||
final pools = await widget.ref.read(poolsProvider.future);
|
||||
final selectedPool = pools.filterValid().firstWhere(
|
||||
(p) => p.id == selectedPoolId,
|
||||
);
|
||||
|
||||
// Check constraints
|
||||
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
|
||||
final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize;
|
||||
|
||||
final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?;
|
||||
final mimeType =
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(attachment.type);
|
||||
final typeAccepted =
|
||||
acceptTypes == null ||
|
||||
acceptTypes.isEmpty ||
|
||||
acceptTypes.any((type) => mimeType.startsWith(type));
|
||||
|
||||
final hasConstraints = fileSizeExceeded || !typeAccepted;
|
||||
|
||||
return AttachmentUploadConfig(
|
||||
poolId: selectedPoolId!,
|
||||
hasConstraints: hasConstraints,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmUpload() async {
|
||||
final config = await _getUploadConfig();
|
||||
if (config != null && mounted) {
|
||||
Navigator.pop(context, config);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _getFileSize(UniversalFile attachment) async {
|
||||
if (attachment.data is XFile) {
|
||||
try {
|
||||
return await (attachment.data as XFile).length();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else if (attachment.data is SnCloudFile) {
|
||||
return (attachment.data as SnCloudFile).size;
|
||||
} else if (attachment.data is List<int>) {
|
||||
return (attachment.data as List<int>).length;
|
||||
} else if (attachment.data is Uint8List) {
|
||||
return (attachment.data as Uint8List).length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _formatNumber(int number) {
|
||||
if (number >= 1000000) {
|
||||
return '${(number / 1000000).toStringAsFixed(1)}M';
|
||||
} else if (number >= 1000) {
|
||||
return '${(number / 1000).toStringAsFixed(1)}K';
|
||||
} else {
|
||||
return number.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes >= 1073741824) {
|
||||
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
|
||||
} else if (bytes >= 1048576) {
|
||||
return '${(bytes / 1048576).toStringAsFixed(1)} MB';
|
||||
} else if (bytes >= 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
} else {
|
||||
return '$bytes bytes';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatQuotaCost(int fileSize, SnFilePool pool) {
|
||||
final costMultiplier = pool.billingConfig?['cost_multiplier'] ?? 1.0;
|
||||
final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round();
|
||||
return _formatNumber(quotaCost);
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ 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/pods/messages_notifier.dart';
|
||||
import 'package:island/pods/chat/messages_notifier.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
@@ -6,7 +6,7 @@ part of 'file_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$billingUsageHash() => r'270ec8499378ee0c038aa44ad1c2e3ad9025740a';
|
||||
String _$billingUsageHash() => r'58d8bc774868d60781574c85d6b25869a79c57aa';
|
||||
|
||||
/// See also [billingUsage].
|
||||
@ProviderFor(billingUsage)
|
||||
@@ -25,7 +25,7 @@ final billingUsageProvider =
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef BillingUsageRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$billingQuotaHash() => r'0696b500fa8bb1270641bcacf262be58caff9b38';
|
||||
String _$billingQuotaHash() => r'4ec5d728e439015800abb2d0d673b5a7329cc654';
|
||||
|
||||
/// See also [billingQuota].
|
||||
@ProviderFor(billingQuota)
|
||||
@@ -45,7 +45,7 @@ final billingQuotaProvider =
|
||||
// ignore: unused_element
|
||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$cloudFileListNotifierHash() =>
|
||||
r'e2c8a076a9e635c7b43a87d00f78775427ba6334';
|
||||
r'22c45a8ea23147a3835ba870ad2f0bb833f853ea';
|
||||
|
||||
/// See also [CloudFileListNotifier].
|
||||
@ProviderFor(CloudFileListNotifier)
|
||||
|
@@ -12,6 +12,7 @@ import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/widgets/post/post_award_sheet.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/post/post_award_history_sheet.dart';
|
||||
import 'package:island/widgets/post/post_pin_sheet.dart';
|
||||
@@ -273,7 +274,14 @@ class PostActionButtons extends HookConsumerWidget {
|
||||
|
||||
actions.add(
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => PostAwardSheet(post: post),
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
@@ -22,7 +22,6 @@ 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';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
|
||||
class SettingsScreen extends HookConsumerWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -93,6 +92,48 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Theme mode settings
|
||||
ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsThemeMode').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.dark_mode),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'system',
|
||||
child: Text('settingsThemeModeSystem').tr().fontSize(14),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'light',
|
||||
child: Text('settingsThemeModeLight').tr().fontSize(14),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'dark',
|
||||
child: Text('settingsThemeModeDark').tr().fontSize(14),
|
||||
),
|
||||
],
|
||||
value: settings.themeMode,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setThemeMode(value);
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
height: 40,
|
||||
width: 140,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(height: 40),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Custom fonts settings
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
@@ -417,7 +458,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
if (user.value != null)
|
||||
pools.when(
|
||||
data: (data) {
|
||||
final validPools = data.filterValid();
|
||||
final validPools = data;
|
||||
final currentPoolId = resolveDefaultPoolId(ref, data);
|
||||
|
||||
return ListTile(
|
||||
@@ -437,11 +478,14 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
validPools.map((p) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: p.id,
|
||||
child: Text(
|
||||
p.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).fontSize(14),
|
||||
child: Tooltip(
|
||||
message: p.name,
|
||||
child: Text(
|
||||
p.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).fontSize(14),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
value: currentPoolId,
|
||||
@@ -577,8 +621,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,15 +47,10 @@ class TrayService {
|
||||
void handleAction(MenuItem item) {
|
||||
switch (item.key) {
|
||||
case 'show_window':
|
||||
() async {
|
||||
appWindow.show();
|
||||
appWindow.restore();
|
||||
await Future.delayed(const Duration(milliseconds: 32));
|
||||
appWindow.show();
|
||||
}();
|
||||
windowManager.show();
|
||||
break;
|
||||
case 'exit_app':
|
||||
appWindow.close();
|
||||
windowManager.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_app_update/azhon_app_update.dart';
|
||||
@@ -233,8 +234,208 @@ class UpdateService {
|
||||
return 'https://fs.solsynth.dev/d/official/solian/build-output-windows-installer.zip';
|
||||
}
|
||||
|
||||
/// Performs automatic Windows update: download, extract, and install
|
||||
Future<void> performAutomaticWindowsUpdate(
|
||||
BuildContext context,
|
||||
String url,
|
||||
) async {
|
||||
if (!context.mounted) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => _WindowsUpdateDialog(
|
||||
updateUrl: url,
|
||||
onComplete: () {
|
||||
// Close the update sheet
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch the latest release info from GitHub.
|
||||
/// Public so other screens (e.g., About) can manually trigger update checks.
|
||||
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
||||
final apiEndpoint =
|
||||
useProxy
|
||||
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
|
||||
: _releasesLatestApi;
|
||||
|
||||
log(
|
||||
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
|
||||
);
|
||||
final resp = await _dio.get(apiEndpoint);
|
||||
if (resp.statusCode != 200) {
|
||||
log(
|
||||
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final data = resp.data as Map<String, dynamic>;
|
||||
log('[Update] Successfully fetched release data.');
|
||||
|
||||
final tagName = (data['tag_name'] ?? '').toString();
|
||||
final name = (data['name'] ?? tagName).toString();
|
||||
final body = (data['body'] ?? '').toString();
|
||||
final htmlUrl = (data['html_url'] ?? '').toString();
|
||||
final createdAtStr = (data['created_at'] ?? '').toString();
|
||||
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
||||
final assetsData =
|
||||
(data['assets'] as List<dynamic>?)
|
||||
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (tagName.isEmpty || htmlUrl.isEmpty) {
|
||||
log(
|
||||
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
|
||||
return GithubReleaseInfo(
|
||||
tagName: tagName,
|
||||
name: name,
|
||||
body: body,
|
||||
htmlUrl: htmlUrl,
|
||||
createdAt: createdAt,
|
||||
assets: assetsData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WindowsUpdateDialog extends StatefulWidget {
|
||||
const _WindowsUpdateDialog({
|
||||
required this.updateUrl,
|
||||
required this.onComplete,
|
||||
});
|
||||
|
||||
final String updateUrl;
|
||||
final VoidCallback onComplete;
|
||||
|
||||
@override
|
||||
State<_WindowsUpdateDialog> createState() => _WindowsUpdateDialogState();
|
||||
}
|
||||
|
||||
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
||||
final ValueNotifier<String> messageNotifier = ValueNotifier<String>('Downloading installer...');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startUpdate();
|
||||
}
|
||||
|
||||
Future<void> _startUpdate() async {
|
||||
try {
|
||||
// Step 1: Download
|
||||
final zipPath = await _downloadWindowsInstaller(
|
||||
widget.updateUrl,
|
||||
onProgress: (received, total) {
|
||||
if (total == -1) {
|
||||
progressNotifier.value = null;
|
||||
} else {
|
||||
progressNotifier.value = received / total;
|
||||
}
|
||||
},
|
||||
);
|
||||
if (zipPath == null) {
|
||||
_showError('Failed to download installer');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Extract
|
||||
messageNotifier.value = 'Extracting installer...';
|
||||
progressNotifier.value = null; // Indeterminate for extraction
|
||||
|
||||
final extractDir = await _extractWindowsInstaller(zipPath);
|
||||
if (extractDir == null) {
|
||||
_showError('Failed to extract installer');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Run installer
|
||||
messageNotifier.value = 'Running installer...';
|
||||
|
||||
final success = await _runWindowsInstaller(extractDir);
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
messageNotifier.value = 'Update Complete';
|
||||
progressNotifier.value = 1.0;
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
widget.onComplete();
|
||||
}
|
||||
} else {
|
||||
_showError('Failed to run installer');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
try {
|
||||
await File(zipPath).delete();
|
||||
await Directory(extractDir).delete(recursive: true);
|
||||
} catch (e) {
|
||||
log('[Update] Error cleaning up temporary files: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
_showError('Update failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Update Failed'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Installing Update'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ValueListenableBuilder<double?>(
|
||||
valueListenable: progressNotifier,
|
||||
builder: (context, progress, child) {
|
||||
return LinearProgressIndicator(value: progress);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: messageNotifier,
|
||||
builder: (context, message, child) {
|
||||
return Text(message);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Downloads the Windows installer ZIP file
|
||||
Future<String?> _downloadWindowsInstaller(String url) async {
|
||||
Future<String?> _downloadWindowsInstaller(
|
||||
String url, {
|
||||
void Function(int received, int total)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
log('[Update] Starting Windows installer download from: $url');
|
||||
|
||||
@@ -243,7 +444,7 @@ class UpdateService {
|
||||
'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip';
|
||||
final filePath = path.join(tempDir.path, fileName);
|
||||
|
||||
final response = await _dio.download(
|
||||
final response = await Dio().download(
|
||||
url,
|
||||
filePath,
|
||||
onReceiveProgress: (received, total) {
|
||||
@@ -252,6 +453,7 @@ class UpdateService {
|
||||
'[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%',
|
||||
);
|
||||
}
|
||||
onProgress?.call(received, total);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -311,13 +513,20 @@ class UpdateService {
|
||||
try {
|
||||
log('[Update] Running Windows installer from: $extractDir');
|
||||
|
||||
final setupExePath = path.join(extractDir, 'setup.exe');
|
||||
final dir = Directory(extractDir);
|
||||
final exeFiles = dir
|
||||
.listSync()
|
||||
.where((f) => f is File && f.path.endsWith('.exe'))
|
||||
.toList();
|
||||
|
||||
if (!await File(setupExePath).exists()) {
|
||||
log('[Update] setup.exe not found in extracted directory');
|
||||
if (exeFiles.isEmpty) {
|
||||
log('[Update] No .exe file found in extracted directory');
|
||||
return false;
|
||||
}
|
||||
|
||||
final setupExePath = exeFiles.first.path;
|
||||
log('[Update] Found installer executable: $setupExePath');
|
||||
|
||||
final shell = Shell();
|
||||
final results = await shell.run(setupExePath);
|
||||
final result = results.first;
|
||||
@@ -338,202 +547,6 @@ class UpdateService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs automatic Windows update: download, extract, and install
|
||||
Future<void> _performAutomaticWindowsUpdate(
|
||||
BuildContext context,
|
||||
String url,
|
||||
) async {
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Show progress dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder:
|
||||
(context) => const AlertDialog(
|
||||
title: Text('Installing Update'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Downloading installer...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
// Step 1: Download
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(); // Close progress dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder:
|
||||
(context) => const AlertDialog(
|
||||
title: Text('Installing Update'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Extracting installer...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final zipPath = await _downloadWindowsInstaller(url);
|
||||
if (zipPath == null) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
_showErrorDialog(context, 'Failed to download installer');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Extract
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(); // Close progress dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder:
|
||||
(context) => const AlertDialog(
|
||||
title: Text('Installing Update'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Running installer...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final extractDir = await _extractWindowsInstaller(zipPath);
|
||||
if (extractDir == null) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
_showErrorDialog(context, 'Failed to extract installer');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Run installer
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(); // Close progress dialog
|
||||
|
||||
final success = await _runWindowsInstaller(extractDir);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (success) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Update Complete'),
|
||||
content: const Text(
|
||||
'The application has been updated successfully. Please restart the application.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
// Close the update sheet
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_showErrorDialog(context, 'Failed to run installer');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
try {
|
||||
await File(zipPath).delete();
|
||||
await Directory(extractDir).delete(recursive: true);
|
||||
} catch (e) {
|
||||
log('[Update] Error cleaning up temporary files: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(); // Close any open dialogs
|
||||
_showErrorDialog(context, 'Update failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorDialog(BuildContext context, String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Update Failed'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch the latest release info from GitHub.
|
||||
/// Public so other screens (e.g., About) can manually trigger update checks.
|
||||
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
||||
final apiEndpoint =
|
||||
useProxy
|
||||
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
|
||||
: _releasesLatestApi;
|
||||
|
||||
log(
|
||||
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
|
||||
);
|
||||
final resp = await _dio.get(apiEndpoint);
|
||||
if (resp.statusCode != 200) {
|
||||
log(
|
||||
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final data = resp.data as Map<String, dynamic>;
|
||||
log('[Update] Successfully fetched release data.');
|
||||
|
||||
final tagName = (data['tag_name'] ?? '').toString();
|
||||
final name = (data['name'] ?? tagName).toString();
|
||||
final body = (data['body'] ?? '').toString();
|
||||
final htmlUrl = (data['html_url'] ?? '').toString();
|
||||
final createdAtStr = (data['created_at'] ?? '').toString();
|
||||
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
||||
final assetsData =
|
||||
(data['assets'] as List<dynamic>?)
|
||||
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (tagName.isEmpty || htmlUrl.isEmpty) {
|
||||
log(
|
||||
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
|
||||
return GithubReleaseInfo(
|
||||
tagName: tagName,
|
||||
name: name,
|
||||
body: body,
|
||||
htmlUrl: htmlUrl,
|
||||
createdAt: createdAt,
|
||||
assets: assetsData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateSheet extends StatefulWidget {
|
||||
@@ -584,7 +597,7 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SheetScaffold(
|
||||
titleText: 'Update available',
|
||||
titleText: 'updateAvailable'.tr(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
@@ -612,14 +625,14 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
||||
child: MarkdownTextContent(
|
||||
content:
|
||||
widget.release.body.isEmpty
|
||||
? 'No changelog provided.'
|
||||
? 'noChangelogProvided'.tr()
|
||||
: widget.release.body,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb && Platform.isAndroid)
|
||||
SwitchListTile(
|
||||
title: const Text('Use secondary source for download'),
|
||||
title: Text('useSecondarySourceForDownload'.tr()),
|
||||
value: _useProxy,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@@ -642,7 +655,7 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
||||
_installUpdate(widget.androidUpdateUrl!);
|
||||
},
|
||||
icon: const Icon(Symbols.update),
|
||||
label: const Text('Install update'),
|
||||
label: Text('installUpdate'.tr()),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb &&
|
||||
@@ -655,20 +668,20 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
||||
final updateService = UpdateService(
|
||||
useProxy: widget.useProxy,
|
||||
);
|
||||
updateService._performAutomaticWindowsUpdate(
|
||||
updateService.performAutomaticWindowsUpdate(
|
||||
context,
|
||||
widget.windowsUpdateUrl!,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.update),
|
||||
label: const Text('Install update'),
|
||||
label: Text('installUpdate'.tr()),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: widget.onOpen,
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
label: const Text('Open release page'),
|
||||
label: Text('openReleasePage'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
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,
|
||||
);
|
||||
}
|
@@ -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),
|
||||
|
@@ -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
|
||||
@@ -31,16 +31,19 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isMaximized = useState(false);
|
||||
|
||||
// Add window resize listener for desktop platforms
|
||||
useEffect(() {
|
||||
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
|
||||
@@ -48,11 +51,16 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
_WindowSizeObserver(saveWindowSize),
|
||||
);
|
||||
|
||||
final maximizeListener = _WindowMaximizeListener(isMaximized);
|
||||
windowManager.addListener(maximizeListener);
|
||||
windowManager.isMaximized().then((max) => isMaximized.value = max);
|
||||
|
||||
return () {
|
||||
// Cleanup observer when widget is disposed
|
||||
WidgetsBinding.instance.removeObserver(
|
||||
_WindowSizeObserver(saveWindowSize),
|
||||
);
|
||||
windowManager.removeListener(maximizeListener);
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -61,13 +69,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 +76,80 @@ 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),
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
MinimizeWindowButton(colors: windowButtonColor),
|
||||
if (!Platform.isMacOS)
|
||||
MaximizeWindowButton(colors: windowButtonColor),
|
||||
if (!Platform.isMacOS)
|
||||
CloseWindowButton(
|
||||
colors: windowButtonColor,
|
||||
onPressed: () => appWindow.hide(),
|
||||
),
|
||||
child: DragToMoveArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment:
|
||||
Platform.isMacOS
|
||||
? MainAxisAlignment.center
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Platform.isMacOS
|
||||
? Text(
|
||||
'Solar Network',
|
||||
textAlign: TextAlign.center,
|
||||
).padding(horizontal: 12, vertical: 5)
|
||||
: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? 'assets/icons/icon-dark.png'
|
||||
: 'assets/icons/icon.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Solar Network',
|
||||
textAlign: 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(isMaximized.value ? Symbols.fullscreen_exit : Symbols.fullscreen),
|
||||
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,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Symbols.close),
|
||||
onPressed: () => windowManager.hide(),
|
||||
iconSize: 16,
|
||||
padding: EdgeInsets.all(8),
|
||||
constraints: BoxConstraints(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -162,6 +199,21 @@ class _WindowSizeObserver extends WidgetsBindingObserver {
|
||||
int get hashCode => onSaveWindowSize.hashCode;
|
||||
}
|
||||
|
||||
class _WindowMaximizeListener with WindowListener {
|
||||
final ValueNotifier<bool> isMaximized;
|
||||
_WindowMaximizeListener(this.isMaximized);
|
||||
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
isMaximized.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUnmaximize() {
|
||||
isMaximized.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
final rootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
class AppScaffold extends HookConsumerWidget {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -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() {
|
||||
|
@@ -26,15 +26,20 @@ class AttachmentUploadConfig {
|
||||
|
||||
class AttachmentUploaderSheet extends StatefulWidget {
|
||||
final WidgetRef ref;
|
||||
final ComposeState state;
|
||||
final ComposeState? state;
|
||||
final List<UniversalFile>? attachments;
|
||||
final int index;
|
||||
|
||||
const AttachmentUploaderSheet({
|
||||
super.key,
|
||||
required this.ref,
|
||||
required this.state,
|
||||
this.state,
|
||||
this.attachments,
|
||||
required this.index,
|
||||
});
|
||||
}) : assert(
|
||||
state != null || attachments != null,
|
||||
'Either state or attachments must be provided',
|
||||
);
|
||||
|
||||
@override
|
||||
State<AttachmentUploaderSheet> createState() =>
|
||||
@@ -46,7 +51,9 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final attachment = widget.state.attachments.value[widget.index];
|
||||
final attachment =
|
||||
widget.attachments?[widget.index] ??
|
||||
widget.state!.attachments.value[widget.index];
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'uploadAttachment'.tr(),
|
||||
@@ -59,7 +66,7 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('errorLoadingPools'.tr()));
|
||||
}
|
||||
final pools = snapshot.data!.filterValid();
|
||||
final pools = snapshot.data!;
|
||||
selectedPoolId ??= resolveDefaultPoolId(widget.ref, pools);
|
||||
|
||||
return Column(
|
||||
@@ -111,19 +118,18 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
||||
|
||||
// Check accepted types
|
||||
final acceptTypes =
|
||||
selectedPool.policyConfig?['accept_types']
|
||||
as List?;
|
||||
(selectedPool.policyConfig?['accept_types']
|
||||
as List?)
|
||||
?.cast<String>();
|
||||
final mimeType =
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(
|
||||
attachment.type,
|
||||
);
|
||||
final typeAccepted =
|
||||
acceptTypes == null ||
|
||||
acceptTypes.isEmpty ||
|
||||
acceptTypes.any(
|
||||
(type) => mimeType.startsWith(type),
|
||||
);
|
||||
final typeAccepted = _isMimeTypeAccepted(
|
||||
mimeType,
|
||||
acceptTypes,
|
||||
);
|
||||
|
||||
final hasIssues = fileSizeExceeded || !typeAccepted;
|
||||
|
||||
@@ -279,29 +285,27 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
||||
}
|
||||
|
||||
Future<AttachmentUploadConfig?> _getUploadConfig() async {
|
||||
final attachment = widget.state.attachments.value[widget.index];
|
||||
final attachment =
|
||||
widget.attachments?[widget.index] ??
|
||||
widget.state!.attachments.value[widget.index];
|
||||
final fileSize = await _getFileSize(attachment);
|
||||
|
||||
if (fileSize == null) return null;
|
||||
|
||||
// Get the selected pool to check constraints
|
||||
final pools = await widget.ref.read(poolsProvider.future);
|
||||
final selectedPool = pools.filterValid().firstWhere(
|
||||
(p) => p.id == selectedPoolId,
|
||||
);
|
||||
final selectedPool = pools.firstWhere((p) => p.id == selectedPoolId);
|
||||
|
||||
// Check constraints
|
||||
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
|
||||
final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize;
|
||||
|
||||
final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?;
|
||||
final acceptTypes =
|
||||
(selectedPool.policyConfig?['accept_types'] as List?)?.cast<String>();
|
||||
final mimeType =
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(attachment.type);
|
||||
final typeAccepted =
|
||||
acceptTypes == null ||
|
||||
acceptTypes.isEmpty ||
|
||||
acceptTypes.any((type) => mimeType.startsWith(type));
|
||||
final typeAccepted = _isMimeTypeAccepted(mimeType, acceptTypes);
|
||||
|
||||
final hasConstraints = fileSizeExceeded || !typeAccepted;
|
||||
|
||||
@@ -362,4 +366,16 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
||||
final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round();
|
||||
return _formatNumber(quotaCost);
|
||||
}
|
||||
|
||||
bool _isMimeTypeAccepted(String mimeType, List<String>? acceptTypes) {
|
||||
if (acceptTypes == null || acceptTypes.isEmpty) return true;
|
||||
return acceptTypes.any((type) {
|
||||
if (type.endsWith('/*')) {
|
||||
final mainType = type.substring(0, type.length - 2);
|
||||
return mimeType.startsWith('$mainType/');
|
||||
} else {
|
||||
return mimeType == type;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||
|
@@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_popup_card/flutter_popup_card.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/widgets/account/account_nameplate.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
import 'package:island/widgets/chat/call_participant_card.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
@@ -11,12 +9,14 @@ import "package:image_picker/image_picker.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/widgets/content/attachment_preview.dart";
|
||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||
import "package:pasteboard/pasteboard.dart";
|
||||
import "package:styled_widget/styled_widget.dart";
|
||||
import "package:material_symbols_icons/symbols.dart";
|
||||
import "package:island/widgets/stickers/picker.dart";
|
||||
import "package:island/pods/chat/chat_subscribe.dart";
|
||||
|
||||
class ChatInput extends HookConsumerWidget {
|
||||
final TextEditingController messageController;
|
||||
@@ -55,10 +55,7 @@ class ChatInput extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final inputFocusNode = useFocusNode();
|
||||
|
||||
final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend;
|
||||
|
||||
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||
final chatSubscribe = ref.watch(chatSubscribeNotifierProvider(chatRoom.id));
|
||||
|
||||
void send() {
|
||||
onSend.call();
|
||||
@@ -67,6 +64,18 @@ class ChatInput extends HookConsumerWidget {
|
||||
});
|
||||
}
|
||||
|
||||
void insertNewLine() {
|
||||
final text = messageController.text;
|
||||
final selection = messageController.selection;
|
||||
final start = selection.start >= 0 ? selection.start : text.length;
|
||||
final end = selection.end >= 0 ? selection.end : text.length;
|
||||
final newText = text.replaceRange(start, end, '\n');
|
||||
messageController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(offset: start + 1),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handlePaste() async {
|
||||
final clipboard = await Pasteboard.image;
|
||||
if (clipboard == null) return;
|
||||
@@ -80,212 +89,269 @@ class ChatInput extends HookConsumerWidget {
|
||||
]);
|
||||
}
|
||||
|
||||
void handleKeyPress(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
RawKeyEvent event,
|
||||
) {
|
||||
if (event is! RawKeyDownEvent) return;
|
||||
inputFocusNode.onKeyEvent = (node, event) {
|
||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
|
||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
||||
final isModifierPressed =
|
||||
HardwareKeyboard.instance.isMetaPressed ||
|
||||
HardwareKeyboard.instance.isControlPressed;
|
||||
|
||||
if (isPaste && isModifierPressed) {
|
||||
handlePaste();
|
||||
return;
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
|
||||
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
|
||||
|
||||
if (isEnter) {
|
||||
if (enterToSend && !isModifierPressed) {
|
||||
send();
|
||||
} else if (!enterToSend && isModifierPressed) {
|
||||
if (isModifierPressed) {
|
||||
insertNewLine();
|
||||
return KeyEventResult.handled;
|
||||
} else if (enterToSend) {
|
||||
send();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Material(
|
||||
elevation: 8,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
if (attachments.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 280,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
height: 280,
|
||||
width: 280,
|
||||
child: AttachmentPreview(
|
||||
item: attachments[idx],
|
||||
progress: attachmentProgress['chat-upload']?[idx],
|
||||
onRequestUpload: () => onUploadAttachment(idx),
|
||||
onDelete: () => onDeleteAttachment(idx),
|
||||
onUpdate: (value) {
|
||||
attachments[idx] = value;
|
||||
onAttachmentsChanged(attachments);
|
||||
},
|
||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
final double leftMargin = isWideScreen(context) ? 8 : 16;
|
||||
final double rightMargin = isWideScreen(context) ? leftMargin + 8 : 16;
|
||||
const double bottomMargin = 16;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: leftMargin,
|
||||
right: rightMargin,
|
||||
bottom: bottomMargin,
|
||||
),
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, -0.3),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1.0,
|
||||
child: FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(top: 12),
|
||||
if (messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
margin: const EdgeInsets.only(left: 8, right: 8, top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
messageReplyingTo != null
|
||||
? Symbols.reply
|
||||
: messageForwardingTo != null
|
||||
? Symbols.forward
|
||||
: Symbols.edit,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
messageReplyingTo != null
|
||||
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
|
||||
: messageForwardingTo != null
|
||||
? 'Forwarding message'
|
||||
: 'Editing message',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: onClear,
|
||||
padding: EdgeInsets.zero,
|
||||
style: ButtonStyle(
|
||||
minimumSize: WidgetStatePropertyAll(Size(28, 28)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'stickers'.tr(),
|
||||
icon: const Icon(Symbols.add_reaction),
|
||||
onPressed: () {
|
||||
final size = MediaQuery.of(context).size;
|
||||
showStickerPickerPopover(
|
||||
context,
|
||||
Offset(
|
||||
20,
|
||||
size.height -
|
||||
480 -
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
child:
|
||||
chatSubscribe.isNotEmpty
|
||||
? Container(
|
||||
key: const ValueKey('typing-indicator'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
onPick: (placeholder) {
|
||||
// Insert placeholder at current cursor position
|
||||
final text = messageController.text;
|
||||
final selection = messageController.selection;
|
||||
final start =
|
||||
selection.start >= 0
|
||||
? selection.start
|
||||
: text.length;
|
||||
final end =
|
||||
selection.end >= 0
|
||||
? selection.end
|
||||
: text.length;
|
||||
final newText = text.replaceRange(
|
||||
start,
|
||||
end,
|
||||
placeholder,
|
||||
);
|
||||
messageController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: start + placeholder.length,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.more_horiz,
|
||||
size: 16,
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'typingHint'.plural(
|
||||
chatSubscribe.length,
|
||||
args: [
|
||||
chatSubscribe
|
||||
.map((x) => x.nick ?? x.account.nick)
|
||||
.join(', '),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('typing-indicator-none'),
|
||||
),
|
||||
),
|
||||
if (attachments.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
width: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress: attachmentProgress['chat-upload']?[idx],
|
||||
onRequestUpload: () => onUploadAttachment(idx),
|
||||
onDelete: () => onDeleteAttachment(idx),
|
||||
onUpdate: (value) {
|
||||
attachments[idx] = value;
|
||||
onAttachmentsChanged(attachments);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Symbols.photo_library),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(true),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.photo),
|
||||
Text('addPhoto').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(false),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.video_call),
|
||||
Text('addVideo').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(vertical: 12),
|
||||
if (messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
messageReplyingTo != null
|
||||
? Symbols.reply
|
||||
: messageForwardingTo != null
|
||||
? Symbols.forward
|
||||
: Symbols.edit,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
messageReplyingTo != null
|
||||
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
|
||||
: messageForwardingTo != null
|
||||
? 'Forwarding message'
|
||||
: 'Editing message',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: InkWell(
|
||||
onTap: onClear,
|
||||
child: const Icon(Icons.close, size: 20).center(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (event) => handleKeyPress(context, ref, event),
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'stickers'.tr(),
|
||||
icon: const Icon(Symbols.add_reaction),
|
||||
onPressed: () {
|
||||
final size = MediaQuery.of(context).size;
|
||||
showStickerPickerPopover(
|
||||
context,
|
||||
Offset(
|
||||
20,
|
||||
size.height -
|
||||
480 -
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
onPick: (placeholder) {
|
||||
// Insert placeholder at current cursor position
|
||||
final text = messageController.text;
|
||||
final selection = messageController.selection;
|
||||
final start =
|
||||
selection.start >= 0
|
||||
? selection.start
|
||||
: text.length;
|
||||
final end =
|
||||
selection.end >= 0
|
||||
? selection.end
|
||||
: text.length;
|
||||
final newText = text.replaceRange(
|
||||
start,
|
||||
end,
|
||||
placeholder,
|
||||
);
|
||||
messageController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: start + placeholder.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Symbols.photo_library),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(true),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.photo),
|
||||
Text('addPhoto').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(false),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.video_call),
|
||||
Text('addVideo').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
focusNode: inputFocusNode,
|
||||
controller: messageController,
|
||||
onSubmitted:
|
||||
(enterToSend && isMobile)
|
||||
? (_) {
|
||||
send();
|
||||
}
|
||||
: null,
|
||||
keyboardType:
|
||||
(enterToSend && isMobile)
|
||||
? TextInputType.text
|
||||
: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.send,
|
||||
inputFormatters: [
|
||||
if (enterToSend && !isMobile)
|
||||
TextInputFormatter.withFunction((oldValue, newValue) {
|
||||
if (newValue.text.endsWith('\n')) {
|
||||
return oldValue;
|
||||
}
|
||||
return newValue;
|
||||
}),
|
||||
],
|
||||
keyboardType: TextInputType.multiline,
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
(chatRoom.type == 1 && chatRoom.name == null)
|
||||
@@ -314,16 +380,16 @@ class ChatInput extends HookConsumerWidget {
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: send,
|
||||
),
|
||||
],
|
||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: send,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:pretty_diff_text/pretty_diff_text.dart';
|
||||
@@ -56,7 +56,7 @@ class MessageContent extends StatelessWidget {
|
||||
case 'messages.update.links':
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.edit,
|
||||
@@ -64,27 +64,29 @@ class MessageContent extends StatelessWidget {
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||
),
|
||||
).padding(top: 2),
|
||||
const Gap(4),
|
||||
if (item.meta['previous_content'] is String)
|
||||
PrettyDiffText(
|
||||
oldText: item.meta['previous_content'],
|
||||
newText: item.content ?? 'Edited a message',
|
||||
defaultTextStyle: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
addedTextStyle: TextStyle(
|
||||
backgroundColor: Theme.of(
|
||||
Flexible(
|
||||
child: PrettyDiffText(
|
||||
oldText: item.meta['previous_content'],
|
||||
newText: item.content ?? 'Edited a message',
|
||||
defaultTextStyle: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryFixedDim.withOpacity(0.4),
|
||||
),
|
||||
deletedTextStyle: TextStyle(
|
||||
decoration: TextDecoration.lineThrough,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.7),
|
||||
).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
addedTextStyle: TextStyle(
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryFixedDim.withOpacity(0.4),
|
||||
),
|
||||
deletedTextStyle: TextStyle(
|
||||
decoration: TextDecoration.lineThrough,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
@@ -104,10 +106,12 @@ class MessageContent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MarkdownTextContent(
|
||||
content: item.content ?? '*${item.type} has no content*',
|
||||
isSelectable: true,
|
||||
linesMargin: EdgeInsets.zero,
|
||||
Flexible(
|
||||
child: MarkdownTextContent(
|
||||
content: item.content ?? '*${item.type} has no content*',
|
||||
isSelectable: true,
|
||||
linesMargin: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
if (translatedText?.isNotEmpty ?? false)
|
||||
...([
|
||||
|
@@ -11,10 +11,10 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/database/message.dart';
|
||||
import 'package:island/models/embed.dart';
|
||||
import 'package:island/pods/messages_notifier.dart';
|
||||
import 'package:island/pods/chat/chat_rooms.dart';
|
||||
import 'package:island/pods/chat/messages_notifier.dart';
|
||||
import 'package:island/pods/translate.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
import 'package:island/utils/mapping.dart';
|
||||
import 'package:island/widgets/account/account_pfc.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
@@ -130,7 +130,7 @@ class MessageItem extends HookConsumerWidget {
|
||||
|
||||
useEffect(() {
|
||||
if (flashing) {
|
||||
if (flashTimer.value != null) return null;
|
||||
flashTimer.value?.cancel();
|
||||
isFlashing.value = true;
|
||||
flashTimer.value = Timer.periodic(
|
||||
const Duration(milliseconds: kFlashDuration),
|
||||
@@ -343,6 +343,10 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
|
||||
isCurrentUser
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
final containerColor =
|
||||
isCurrentUser
|
||||
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
|
||||
: Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
final hasBackground =
|
||||
ref.watch(backgroundImageFileProvider).valueOrNull != null;
|
||||
@@ -377,98 +381,108 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (remoteMessage.repliedMessageId != null)
|
||||
MessageQuoteWidget(
|
||||
message: message,
|
||||
textColor: textColor,
|
||||
isReply: true,
|
||||
).padding(vertical: 4),
|
||||
if (remoteMessage.forwardedMessageId != null)
|
||||
MessageQuoteWidget(
|
||||
message: message,
|
||||
textColor: textColor,
|
||||
isReply: false,
|
||||
).padding(vertical: 4),
|
||||
if (MessageContent.hasContent(remoteMessage))
|
||||
MessageContent(
|
||||
item: remoteMessage,
|
||||
translatedText: translatedText,
|
||||
),
|
||||
if (remoteMessage.attachments.isNotEmpty)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return CloudFileList(
|
||||
files: remoteMessage.attachments,
|
||||
maxWidth: constraints.maxWidth,
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (remoteMessage.meta['embeds'] != null)
|
||||
...((remoteMessage.meta['embeds'] as List<dynamic>)
|
||||
.map((embed) => convertMapKeysToSnakeCase(embed))
|
||||
.where((embed) => embed['type'] == 'link')
|
||||
.map((embed) => SnScrappedLink.fromJson(embed))
|
||||
.map(
|
||||
(link) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return EmbedLinkWidget(
|
||||
link: link,
|
||||
maxWidth: math.min(
|
||||
constraints.maxWidth,
|
||||
480,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: containerColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (remoteMessage.repliedMessageId != null)
|
||||
MessageQuoteWidget(
|
||||
message: message,
|
||||
textColor: textColor,
|
||||
isReply: true,
|
||||
).padding(vertical: 4),
|
||||
if (remoteMessage.forwardedMessageId != null)
|
||||
MessageQuoteWidget(
|
||||
message: message,
|
||||
textColor: textColor,
|
||||
isReply: false,
|
||||
).padding(vertical: 4),
|
||||
if (MessageContent.hasContent(remoteMessage))
|
||||
MessageContent(
|
||||
item: remoteMessage,
|
||||
translatedText: translatedText,
|
||||
),
|
||||
if (remoteMessage.attachments.isNotEmpty)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return CloudFileList(
|
||||
files: remoteMessage.attachments,
|
||||
maxWidth: constraints.maxWidth,
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (remoteMessage.meta['embeds'] != null)
|
||||
...((remoteMessage.meta['embeds'] as List<dynamic>)
|
||||
.map((embed) => convertMapKeysToSnakeCase(embed))
|
||||
.where((embed) => embed['type'] == 'link')
|
||||
.map((embed) => SnScrappedLink.fromJson(embed))
|
||||
.map(
|
||||
(link) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return EmbedLinkWidget(
|
||||
link: link,
|
||||
maxWidth: math.min(
|
||||
constraints.maxWidth,
|
||||
480,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList()),
|
||||
if (progress != null && progress!.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if ((remoteMessage.content?.isNotEmpty ?? false))
|
||||
const Gap(0),
|
||||
for (var entry in progress!.entries)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'fileUploadingProgress'.tr(
|
||||
args: [
|
||||
(entry.key + 1).toString(),
|
||||
entry.value.toStringAsFixed(1),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
const Gap(4),
|
||||
LinearProgressIndicator(
|
||||
value: entry.value / 100,
|
||||
backgroundColor:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceVariant,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList()),
|
||||
if (progress != null && progress!.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if ((remoteMessage.content?.isNotEmpty ?? false))
|
||||
],
|
||||
),
|
||||
const Gap(0),
|
||||
for (var entry in progress!.entries)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'fileUploadingProgress'.tr(
|
||||
args: [
|
||||
(entry.key + 1).toString(),
|
||||
entry.value.toStringAsFixed(1),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
LinearProgressIndicator(
|
||||
value: entry.value / 100,
|
||||
backgroundColor:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceVariant,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(0),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
MessageIndicators(
|
||||
@@ -524,7 +538,7 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
|
||||
isMultiline ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('HH:mm').format(message.createdAt),
|
||||
DateFormat('HH:mm').format(message.createdAt.toLocal()),
|
||||
style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12),
|
||||
).padding(top: isMultiline ? 2 : 0),
|
||||
AccountPfcGestureDetector(
|
||||
|
@@ -6,7 +6,7 @@ import "package:gap/gap.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
|
@@ -229,6 +229,7 @@ class CheckInWidget extends HookConsumerWidget {
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
spacing: 4,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
|
@@ -21,7 +21,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'File Information',
|
||||
titleText: 'fileInfoTitle'.tr(),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -81,7 +81,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
),
|
||||
onLongPress: () {
|
||||
Clipboard.setData(ClipboardData(text: item.hash!));
|
||||
showSnackBar('File hash copied to clipboard');
|
||||
showSnackBar('fileHashCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -101,7 +101,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.id));
|
||||
showSnackBar('File ID copied to clipboard');
|
||||
showSnackBar('fileIdCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -118,10 +118,28 @@ class FileInfoSheet extends StatelessWidget {
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.name));
|
||||
showSnackBar('File name copied to clipboard');
|
||||
showSnackBar('fileNameCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
if (item.pool != null)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.calendar_today),
|
||||
title: Text('File Pool').tr(),
|
||||
subtitle: Text(
|
||||
item.pool!.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.pool!.id));
|
||||
showSnackBar('fileNameCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
if (exifData.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Theme(
|
||||
@@ -163,7 +181,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: '${entry.value}'),
|
||||
);
|
||||
showSnackBar('Value copied to clipboard');
|
||||
showSnackBar('valueCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -180,7 +198,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(
|
||||
'File Metadata',
|
||||
'fileMetadata'.tr(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -212,7 +230,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: jsonEncode(entry.value)),
|
||||
);
|
||||
showSnackBar('Value copied to clipboard');
|
||||
showSnackBar('valueCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -229,7 +247,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(
|
||||
'User Metadata',
|
||||
'userMetadata'.tr(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -261,7 +279,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: jsonEncode(entry.value)),
|
||||
);
|
||||
showSnackBar('Value copied to clipboard');
|
||||
showSnackBar('valueCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@@ -1,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/message.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/services/update_service.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/network_status_sheet.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -65,69 +68,97 @@ class DebugSheet extends HookConsumerWidget {
|
||||
return SheetScaffold(
|
||||
titleText: 'Debug',
|
||||
heightFactor: 0.6,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.wifi),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('Connection Status'),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => NetworkStatusSheet(
|
||||
onReconnect: () => wsNotifier.connect(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.copy_all),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Copy access token'),
|
||||
onTap: () async {
|
||||
final tk = ref.watch(tokenProvider);
|
||||
Clipboard.setData(ClipboardData(text: tk!.token));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Set access token'),
|
||||
onTap: () async {
|
||||
await _showSetTokenDialog(context, ref);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.delete),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Reset database'),
|
||||
onTap: () async {
|
||||
resetDatabase(ref);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.clear),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Clear cache'),
|
||||
onTap: () async {
|
||||
DefaultCacheManager().emptyCache();
|
||||
},
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(4),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.update),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('Force Update'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () async {
|
||||
// Fetch latest release and show the unified sheet
|
||||
final svc = UpdateService();
|
||||
// Reuse service fetch + compare to decide content
|
||||
showLoadingModal(context);
|
||||
final release = await svc.fetchLatestRelease();
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
if (release != null) {
|
||||
await svc.showUpdateSheet(context, release);
|
||||
} else {
|
||||
showInfoAlert(
|
||||
'Currently cannot get update from the GitHub.',
|
||||
'Unable to check for updates',
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
const Divider(height: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.wifi),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('Connection Status'),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => NetworkStatusSheet(
|
||||
onReconnect: () => wsNotifier.connect(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.copy_all),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Copy access token'),
|
||||
onTap: () async {
|
||||
final tk = ref.watch(tokenProvider);
|
||||
Clipboard.setData(ClipboardData(text: tk!.token));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Set access token'),
|
||||
onTap: () async {
|
||||
await _showSetTokenDialog(context, ref);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.delete),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Reset database'),
|
||||
onTap: () async {
|
||||
resetDatabase(ref);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.clear),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Clear cache'),
|
||||
onTap: () async {
|
||||
DefaultCacheManager().emptyCache();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -80,9 +81,8 @@ class PostItemCreator extends HookConsumerWidget {
|
||||
title: 'copyLink'.tr(),
|
||||
image: MenuImage.icon(Symbols.link),
|
||||
callback: () {
|
||||
context.pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: 'https://solian.app/posts/${item.id}'),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -95,7 +95,7 @@ class PostItemCreator extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
if (isOpenable) {
|
||||
context.goNamed('postDetail', pathParameters: {'id': item.id});
|
||||
context.pushNamed('postDetail', pathParameters: {'id': item.id});
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
|
||||
@@ -20,16 +19,15 @@
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <record_linux/record_linux_plugin.h>
|
||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <super_native_extensions/super_native_extensions_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <volume_controller/volume_controller_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
|
||||
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) file_saver_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
||||
file_saver_plugin_register_with_registrar(file_saver_registrar);
|
||||
@@ -69,6 +67,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) record_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
|
||||
record_linux_plugin_register_with_registrar(record_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
||||
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
||||
@@ -84,4 +85,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
|
||||
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_linux
|
||||
file_saver
|
||||
file_selector_linux
|
||||
flutter_platform_alert
|
||||
@@ -17,11 +16,13 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_video
|
||||
pasteboard
|
||||
record_linux
|
||||
screen_retriever_linux
|
||||
sqlite3_flutter_libs
|
||||
super_native_extensions
|
||||
tray_manager
|
||||
url_launcher_linux
|
||||
volume_controller
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@@ -6,19 +6,20 @@
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||
|
||||
struct _MyApplication {
|
||||
struct _MyApplication
|
||||
{
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
char **dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
static void my_application_activate(GApplication *application)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(application);
|
||||
GtkWindow *window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
@@ -30,32 +31,36 @@ static void my_application_activate(GApplication* application) {
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
GdkScreen *screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen))
|
||||
{
|
||||
const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0)
|
||||
{
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
if (use_header_bar)
|
||||
{
|
||||
GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "island");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_window_set_title(window, "island");
|
||||
}
|
||||
|
||||
auto bdw = bitsdojo_window_from(window);
|
||||
bdw->setCustomFrame(true);
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
FlView *view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
@@ -65,16 +70,18 @@ static void my_application_activate(GApplication* application) {
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
if (!g_application_register(application, nullptr, &error))
|
||||
{
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
@@ -84,8 +91,9 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
//MyApplication* self = MY_APPLICATION(object);
|
||||
static void my_application_startup(GApplication *application)
|
||||
{
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
@@ -93,8 +101,9 @@ static void my_application_startup(GApplication* application) {
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
//MyApplication* self = MY_APPLICATION(object);
|
||||
static void my_application_shutdown(GApplication *application)
|
||||
{
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
@@ -102,13 +111,15 @@ static void my_application_shutdown(GApplication* application) {
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
static void my_application_dispose(GObject *object)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
static void my_application_class_init(MyApplicationClass *klass)
|
||||
{
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
@@ -116,9 +127,10 @@ static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
static void my_application_init(MyApplication *self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
MyApplication *my_application_new()
|
||||
{
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
// like GTK and desktop environments map this running application to its
|
||||
// corresponding .desktop file. This ensures better integration by allowing
|
||||
|
@@ -5,7 +5,6 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import bitsdojo_window_macos
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
@@ -32,6 +31,7 @@ import package_info_plus
|
||||
import pasteboard
|
||||
import path_provider_foundation
|
||||
import record_macos
|
||||
import screen_retriever_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sign_in_with_apple
|
||||
@@ -42,9 +42,9 @@ import tray_manager
|
||||
import url_launcher_macos
|
||||
import volume_controller
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
@@ -71,6 +71,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
||||
@@ -81,4 +82,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
PODS:
|
||||
- bitsdojo_window_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
@@ -21,19 +19,19 @@ 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)
|
||||
- FlutterMacOS
|
||||
- firebase_core (4.1.0):
|
||||
- firebase_core (4.1.1):
|
||||
- Firebase/CoreOnly (~> 12.2.0)
|
||||
- FlutterMacOS
|
||||
- firebase_crashlytics (5.0.1):
|
||||
- firebase_crashlytics (5.0.2):
|
||||
- Firebase/CoreOnly (~> 12.2.0)
|
||||
- Firebase/Crashlytics (~> 12.2.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (16.0.1):
|
||||
- firebase_messaging (16.0.2):
|
||||
- Firebase/CoreOnly (~> 12.2.0)
|
||||
- Firebase/Messaging (~> 12.2.0)
|
||||
- firebase_core
|
||||
@@ -202,6 +200,8 @@ PODS:
|
||||
- record_macos (1.1.0):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_retriever_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- share_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -248,9 +248,10 @@ PODS:
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (137.7151.04)
|
||||
- window_manager (0.5.0):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
@@ -279,6 +280,7 @@ DEPENDENCIES:
|
||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`)
|
||||
@@ -289,6 +291,7 @@ DEPENDENCIES:
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -314,8 +317,6 @@ SPEC REPOS:
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
bitsdojo_window_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||
croppy:
|
||||
@@ -372,6 +373,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
record_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||
screen_retriever_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
|
||||
share_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
shared_preferences_foundation:
|
||||
@@ -392,9 +395,10 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
window_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
|
||||
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
@@ -402,10 +406,10 @@ SPEC CHECKSUMS:
|
||||
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
|
||||
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
||||
firebase_analytics: efe6e51156f4565f3791d99072e8e3b0fcca0e91
|
||||
firebase_core: a8d3b82b0a87bd1d0ebc21e686b37e939c56e6e1
|
||||
firebase_crashlytics: fdbe67a1229a9e583ebf2b155541491aa83927bb
|
||||
firebase_messaging: 6fb526705903e2e56e38a6ff56b43668b052b01b
|
||||
firebase_analytics: 26346c2ccb9ba410c2f33d5d34c62e6369cbbf29
|
||||
firebase_core: 54fd706197e1779d510b297548eee74d3b39577c
|
||||
firebase_crashlytics: 3694b4aca0849f6919244d7bbbb40615f989f46b
|
||||
firebase_messaging: 658f1a6906d80faec2fb20e3aadb81af6b09e441
|
||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
||||
@@ -441,6 +445,7 @@ SPEC CHECKSUMS:
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
|
||||
@@ -453,6 +458,7 @@ SPEC CHECKSUMS:
|
||||
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
||||
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||
|
||||
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f
|
||||
|
||||
|
@@ -4,10 +4,25 @@ import FlutterMacOS
|
||||
@main
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool)
|
||||
-> Bool
|
||||
{
|
||||
if !flag {
|
||||
for window in NSApp.windows {
|
||||
if !window.isVisible {
|
||||
window.setIsVisible(true)
|
||||
}
|
||||
window.makeKeyAndOrderFront(self)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,7 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
import bitsdojo_window_macos
|
||||
|
||||
class MainFlutterWindow: BitsdojoWindow {
|
||||
override func bitsdojo_window_configure() -> UInt {
|
||||
return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
|
||||
}
|
||||
|
||||
class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController()
|
||||
let windowFrame = self.frame
|
||||
@@ -17,4 +12,4 @@ class MainFlutterWindow: BitsdojoWindow {
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
}
|
||||
}
|
156
pubspec.lock
156
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "948f7d74f41dd6f2d563ea9f4c21d7ea764f8e047d2b24138974c19c24d37eb6"
|
||||
sha256: "23d16f00a2da8ffa997c782453c73867b0609bd90435195671a54de38a3566df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.61"
|
||||
version: "1.3.62"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -81,46 +81,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
bitsdojo_window:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bitsdojo_window
|
||||
sha256: "88ef7765dafe52d97d7a3684960fb5d003e3151e662c18645c1641c22b873195"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
bitsdojo_window_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bitsdojo_window_linux
|
||||
sha256: "9519c0614f98be733e0b1b7cb15b827007886f6fe36a4fb62cf3d35b9dd578ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
bitsdojo_window_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bitsdojo_window_macos
|
||||
sha256: f7c5be82e74568c68c5b8449e2c5d8fd12ec195ecd70745a7b9c0f802bb0268f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
bitsdojo_window_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bitsdojo_window_platform_interface
|
||||
sha256: "65daa015a0c6dba749bdd35a0f092e7a8ba8b0766aa0480eb3ef808086f6e27c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
bitsdojo_window_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bitsdojo_window_windows
|
||||
sha256: fa982cf61ede53f483e50b257344a1c250af231a3cdc93a7064dd6dc0d720b68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -485,10 +445,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "6aaea757f53bb035e8a3baedf3d1d53a79d6549a6c13d84f7546509da9372c7c"
|
||||
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.28.1"
|
||||
version: "2.28.2"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -501,10 +461,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: b52bd710f809db11e25259d429d799d034ba1c5224ce6a73fe8419feb980d44c
|
||||
sha256: b7534bf320aac5213259aac120670ba67b63a1fd010505babc436ff86083818f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.6"
|
||||
version: "0.2.7"
|
||||
dropdown_button2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -629,90 +589,90 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_analytics
|
||||
sha256: dde9d6a7b69b07551a77cfb913c81c64804f7602b07541328322c321e73f2a0e
|
||||
sha256: fce78440ab7b95563054039aac5e342088efed9dc009ac6f81d5cac07155d509
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.1"
|
||||
version: "12.0.2"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: "4008d82a58edcbedec34a7b39f457eed24181cb9c89782c104828c42e4c859b2"
|
||||
sha256: "75bdcd2d2635c4cdcd7ec13727527751ddf2f9933e5bf1264a2387920246f3c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.2"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: db2a2e8803f5471a5f89b4abacae95ae27e0644f77526879fb81a2c1abc12b5f
|
||||
sha256: ed5767695b131cdd425ee6d49934dca80689d9df40609c0d0aa8907ee6f0f785
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0+1"
|
||||
version: "0.6.0+2"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "967dae9a65f69377beb9f4ab292ea63ce5befa1ce24682cab1b69ca4b7a46927"
|
||||
sha256: "4dd96f05015c0dcceaa47711394c32971aee70169625d5e2477e7676c01ce0ee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.1.1"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb"
|
||||
sha256: "5873a370f0d232918e23a5a6137dbe4c2c47cf017301f4ea02d9d636e52f60f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
version: "6.0.1"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: f7ee08febc1c4451588ce58ffcf28edaee857e9a196fee88b85deb889990094a
|
||||
sha256: "61a51037312dac781f713308903bb7a1762a7f92f7bc286a3a0947fb2a713b82"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: f2e175a967712ee1f616ab8843390891a315428ba497ce3d256d4c46f32db6f8
|
||||
sha256: a636096df0d2a4bc72397bfc669a4fffc8896016a58de1a6f45a49d9ba064f94
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.2"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: b49b90af4a1fd8f30b58abd90af88371969bea51b62838a4f4e737c2098b725e
|
||||
sha256: "1ccad077a6fc7bace97d8eace263f42e66dc23a23a839de864a4f10ac4a7c264"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.12"
|
||||
version: "3.8.13"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: aad5dcdea5698499b70d74d5a53b1f6a9972f85f97225e4b7ac006dd8d4f9bac
|
||||
sha256: ba12ad0b600e0c939fbb9391e1cd3320a5b5dad5284276b9182fc21eb1e72c2b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.0.1"
|
||||
version: "16.0.2"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "825bc11767bf50a43dccf49b3026f847ec31d0f176139bfc48d662cc128b5014"
|
||||
sha256: b4bade67bfc09fcc56eb012b3fc72b59ca9e2259a34cdfb81b368169770ff536
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.1"
|
||||
version: "4.7.2"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: db8dbdd79921245c4de02407e33cae2d1868683be18a5ba948d2af5311e3ef5d
|
||||
sha256: "8ae4a00d178993feb79603cad324b53696375cbb78805e8eb603fe331866629d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
version: "4.0.2"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1193,18 +1153,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3
|
||||
sha256: c752e2d08d088bf83742cb05bf83003f3e9d276ff1519b5c92f9d5e60e5ddd23
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.2.2"
|
||||
version: "16.2.4"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: ebc94ed30fd13cefd397cb1658b593f21571f014b7d1197eeb41fb95f05d899a
|
||||
sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2125,6 +2085,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever
|
||||
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_linux
|
||||
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_macos
|
||||
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_platform_interface
|
||||
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_windows
|
||||
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screenshot:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2177,10 +2177,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
|
||||
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.12"
|
||||
version: "2.4.13"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2796,6 +2796,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
windows_notification:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
20
pubspec.yaml
20
pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 3.2.0+133
|
||||
version: 3.2.0+134
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
@@ -38,8 +38,7 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_hooks: ^0.21.3+1
|
||||
hooks_riverpod: ^2.6.1
|
||||
bitsdojo_window: ^0.1.6
|
||||
go_router: ^16.2.2
|
||||
go_router: ^16.2.4
|
||||
styled_widget: ^0.4.1
|
||||
shared_preferences: ^2.5.3
|
||||
flutter_riverpod: ^2.6.1
|
||||
@@ -53,7 +52,7 @@ dependencies:
|
||||
flutter_highlight: ^0.7.0
|
||||
uuid: ^4.5.1
|
||||
url_launcher: ^6.3.2
|
||||
google_fonts: ^6.3.1
|
||||
google_fonts: ^6.3.2
|
||||
gap: ^3.0.1
|
||||
cached_network_image: ^3.4.1
|
||||
web: ^1.1.1
|
||||
@@ -79,13 +78,13 @@ dependencies:
|
||||
image_picker_android: ^0.8.13+3
|
||||
super_context_menu: ^0.9.1
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
firebase_messaging: ^16.0.1
|
||||
firebase_messaging: ^16.0.2
|
||||
flutter_udid: ^4.0.0
|
||||
firebase_core: ^4.1.0
|
||||
firebase_core: ^4.1.1
|
||||
web_socket_channel: ^3.0.3
|
||||
material_symbols_icons: ^4.2873.0
|
||||
drift: ^2.28.1
|
||||
drift_flutter: ^0.2.6
|
||||
drift: ^2.28.2
|
||||
drift_flutter: ^0.2.7
|
||||
path: ^1.9.1
|
||||
collection: ^1.19.1
|
||||
markdown_editor_plus: ^0.2.15
|
||||
@@ -135,8 +134,8 @@ dependencies:
|
||||
flutter_app_update: ^3.2.2
|
||||
archive: ^4.0.7
|
||||
process_run: ^1.2.4
|
||||
firebase_crashlytics: ^5.0.1
|
||||
firebase_analytics: ^12.0.1
|
||||
firebase_crashlytics: ^5.0.2
|
||||
firebase_analytics: ^12.0.2
|
||||
material_color_utilities: ^0.11.1
|
||||
screenshot: ^3.0.0
|
||||
flutter_card_swiper: ^7.0.2
|
||||
@@ -153,6 +152,7 @@ dependencies:
|
||||
ffi: ^2.1.4
|
||||
dart_ipc: ^1.0.1
|
||||
pretty_diff_text: ^2.1.0
|
||||
window_manager: ^0.5.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
; ==================================================
|
||||
#define AppVersion "3.2.0"
|
||||
#define BuildNumber "132"
|
||||
#define BuildNumber "134"
|
||||
; ==================================================
|
||||
|
||||
#define FullVersion AppVersion + "." + BuildNumber
|
||||
@@ -49,4 +49,4 @@ Filename: "{app}\Solian.exe"; Description: "Launch Solian"; Flags: nowait postin
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{userappdata}\dev.solsynth\Solian"
|
||||
Type: files; Name: "{group}\Solian.lnk" ;
|
||||
Type: files; Name: "{autodesktop}\Solian.lnk" ;
|
||||
Type: files; Name: "{autodesktop}\Solian.lnk" ;
|
||||
|
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
@@ -26,17 +25,17 @@
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <volume_controller/volume_controller_plugin_c_api.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DartIpcPluginCApiRegisterWithRegistrar(
|
||||
@@ -75,6 +74,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
@@ -87,6 +88,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
VolumeControllerPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||
WindowsNotificationPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_windows
|
||||
connectivity_plus
|
||||
dart_ipc
|
||||
file_saver
|
||||
@@ -23,12 +22,14 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_video
|
||||
pasteboard
|
||||
record_windows
|
||||
screen_retriever_windows
|
||||
share_plus
|
||||
sqlite3_flutter_libs
|
||||
super_native_extensions
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
volume_controller
|
||||
window_manager
|
||||
windows_notification
|
||||
)
|
||||
|
||||
|
@@ -5,14 +5,21 @@
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
_In_ wchar_t *command_line, _In_ int show_command)
|
||||
{
|
||||
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"single_instance_example");
|
||||
if (hwnd != NULL)
|
||||
{
|
||||
::ShowWindow(hwnd, SW_NORMAL);
|
||||
::SetForegroundWindow(hwnd);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())
|
||||
{
|
||||
CreateAndAttachConsole();
|
||||
}
|
||||
|
||||
@@ -30,13 +37,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"Solian", origin, size)) {
|
||||
if (!window.Create(L"Solian", origin, size))
|
||||
{
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||
while (::GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
}
|
||||
|
Reference in New Issue
Block a user