Compare commits
26 Commits
3.2.0+134
...
6377856ae0
Author | SHA1 | Date | |
---|---|---|---|
6377856ae0 | |||
0f1c52b9e3 | |||
6ed6f60fbc | |||
e65a414065 | |||
214d5c4a53 | |||
fe33931304 | |||
113309257e | |||
b95a8b2ed2 | |||
|
e922971a5e | ||
9d5b71bead | |||
890efa2efb | |||
674097e425 | |||
3379dcb7f3
|
|||
eb5a849e1f | |||
4981a23e8e
|
|||
c64d4bacb6
|
|||
838d18013b
|
|||
3f7902e463
|
|||
54560ad5d8
|
|||
0c729db639
|
|||
1fbaac8d88
|
|||
b9dc724f0b
|
|||
a2cc55696f
|
|||
e79f857feb
|
|||
affba29c04
|
|||
756746b144
|
@@ -14,13 +14,13 @@ The backend of the Solar Network is written in Go and is a microservices app. Th
|
|||||||
|
|
||||||
## Commit Messages
|
## 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
|
All the commit message should follow `:[gitmoji]: <commit message>` syntax
|
||||||
|
|
||||||
## Translations & Localization
|
## 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
|
## 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
|
## 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!
|
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": "添加視頻",
|
"addVideo": "添加視頻",
|
||||||
"addPhoto": "添加照片",
|
"addPhoto": "添加照片",
|
||||||
"addFile": "添加文件",
|
"addFile": "添加文件",
|
||||||
"uploadFile": "上傳文件",
|
|
||||||
"settingsDefaultPool": "選擇文件池",
|
|
||||||
"settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池",
|
|
||||||
|
|
||||||
"createDirectMessage": "創建新私人消息",
|
"createDirectMessage": "創建新私人消息",
|
||||||
"gotoDirectMessage": "前往私信",
|
"gotoDirectMessage": "前往私信",
|
||||||
"react": "反應",
|
"react": "反應",
|
||||||
@@ -307,8 +303,7 @@
|
|||||||
"notifications": "通知",
|
"notifications": "通知",
|
||||||
"posts": "帖子",
|
"posts": "帖子",
|
||||||
"settingsBackgroundImage": "背景圖片",
|
"settingsBackgroundImage": "背景圖片",
|
||||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
"settingsBackgroundImageClear": "清除背景圖片",
|
||||||
"settingsBackgroundImageClear": "清除背景圖片",
|
|
||||||
"settingsBackgroundGenerateColor": "從背景圖像生成主題色",
|
"settingsBackgroundGenerateColor": "從背景圖像生成主題色",
|
||||||
"messageNone": "沒有內容可顯示",
|
"messageNone": "沒有內容可顯示",
|
||||||
"unreadMessages": {
|
"unreadMessages": {
|
||||||
@@ -319,8 +314,6 @@
|
|||||||
"settingsRealmCompactView": "緊湊領域視圖",
|
"settingsRealmCompactView": "緊湊領域視圖",
|
||||||
"settingsMixedFeed": "混合動態",
|
"settingsMixedFeed": "混合動態",
|
||||||
"settingsAutoTranslate": "自動翻譯",
|
"settingsAutoTranslate": "自動翻譯",
|
||||||
"settingsDataSavingMode": "低數據模式",
|
|
||||||
"dataSavingHint": "低數據模式",
|
|
||||||
"settingsHideBottomNav": "隱藏底部導航",
|
"settingsHideBottomNav": "隱藏底部導航",
|
||||||
"settingsSoundEffects": "音效",
|
"settingsSoundEffects": "音效",
|
||||||
"settingsAprilFoolFeatures": "愚人節功能",
|
"settingsAprilFoolFeatures": "愚人節功能",
|
||||||
@@ -675,7 +668,6 @@
|
|||||||
"publisherFeatureDevelopDescription": "為你的開發者解鎖包括應用套件,API 及更多開發功能。",
|
"publisherFeatureDevelopDescription": "為你的開發者解鎖包括應用套件,API 及更多開發功能。",
|
||||||
"publisherFeatureDevelopHint": "目前該功能還在開發中,你需要邀請才可解鎖。",
|
"publisherFeatureDevelopHint": "目前該功能還在開發中,你需要邀請才可解鎖。",
|
||||||
"learnMore": "瞭解更多",
|
"learnMore": "瞭解更多",
|
||||||
"discoverWebArticles": "來自站外的文章",
|
|
||||||
"webArticlesStand": "文章亭",
|
"webArticlesStand": "文章亭",
|
||||||
"about": "關於",
|
"about": "關於",
|
||||||
"somethingWentWrong": "發生了一些錯誤",
|
"somethingWentWrong": "發生了一些錯誤",
|
||||||
@@ -698,8 +690,6 @@
|
|||||||
"sharePostPhoto": "通過圖片分享帖子",
|
"sharePostPhoto": "通過圖片分享帖子",
|
||||||
"wouldYouLikeToNavigateToChat": "你想要前往聊天頁面嗎?",
|
"wouldYouLikeToNavigateToChat": "你想要前往聊天頁面嗎?",
|
||||||
"abuseReports": "舉報",
|
"abuseReports": "舉報",
|
||||||
"discoverRealms": "發現領域",
|
|
||||||
"discoverPublishers": "發現發佈者",
|
|
||||||
"membershipCancel": "取消會員訂閱",
|
"membershipCancel": "取消會員訂閱",
|
||||||
"membershipCancelConfirm": "你確定要取消會員訂閱嗎?",
|
"membershipCancelConfirm": "你確定要取消會員訂閱嗎?",
|
||||||
"membershipCancelHint": "你確定要取消會員訂閱嗎?你將不會再次被扣費。你的會員資格將在當前計費週期結束前保持有效。並且你將無法重新訂閱,直到當前訂閱結束。",
|
"membershipCancelHint": "你確定要取消會員訂閱嗎?你將不會再次被扣費。你的會員資格將在當前計費週期結束前保持有效。並且你將無法重新訂閱,直到當前訂閱結束。",
|
||||||
@@ -763,19 +753,19 @@
|
|||||||
"markAsSensitive": "標記為敏感",
|
"markAsSensitive": "標記為敏感",
|
||||||
"fileName": "文件名",
|
"fileName": "文件名",
|
||||||
"sensitiveCategories": {
|
"sensitiveCategories": {
|
||||||
"language": "語言",
|
"language": "Language",
|
||||||
"sexualContent": "色情內容",
|
"sexualContent": "Sexual Content",
|
||||||
"violence": "暴力",
|
"violence": "Violence",
|
||||||
"profanity": "褻瀆",
|
"profanity": "Profanity",
|
||||||
"hateSpeech": "仇恨言論",
|
"hateSpeech": "Hate Speech",
|
||||||
"racism": "種族主義",
|
"racism": "Racism",
|
||||||
"adultContent": "成人內容",
|
"adultContent": "Adult Content",
|
||||||
"drugAbuse": "藥物濫用",
|
"drugAbuse": "Drug Abuse",
|
||||||
"alcoholAbuse": "酗酒",
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
"gambling": "賭博",
|
"gambling": "Gambling",
|
||||||
"selfHarm": "自殘",
|
"selfHarm": "Self-harm",
|
||||||
"childAbuse": "虐待兒童",
|
"childAbuse": "Child Abuse",
|
||||||
"other": "其他"
|
"other": "Other"
|
||||||
},
|
},
|
||||||
"poll": "投票",
|
"poll": "投票",
|
||||||
"pollsRecent": "最近投票",
|
"pollsRecent": "最近投票",
|
||||||
@@ -819,6 +809,159 @@
|
|||||||
"one": "+{} 個文件被摺疊",
|
"one": "+{} 個文件被摺疊",
|
||||||
"other": "+{} 個文件被摺疊"
|
"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": "應用程式詳情",
|
"appDetails": "應用程式詳情",
|
||||||
"secrets": "密鑰",
|
"secrets": "密鑰",
|
||||||
"appNotFound": "找不到應用程式。",
|
"appNotFound": "找不到應用程式。",
|
||||||
@@ -830,5 +973,107 @@
|
|||||||
"newSecretGenerated": "已產生新密鑰",
|
"newSecretGenerated": "已產生新密鑰",
|
||||||
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
|
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
|
||||||
"expiresIn": "過期時間(秒)",
|
"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"
|
||||||
|
}
|
@@ -8,7 +8,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/theme.dart';
|
import 'package:island/pods/theme.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.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:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
@@ -89,31 +89,42 @@ void main() async {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
||||||
doWhenWindowReady(() {
|
await windowManager.ensureInitialized();
|
||||||
const defaultSize = Size(360, 640);
|
|
||||||
|
|
||||||
// Get saved window size from preferences
|
const defaultSize = Size(360, 640);
|
||||||
final savedSizeString = prefs.getString(kAppWindowSize);
|
|
||||||
Size initialSize = defaultSize;
|
|
||||||
|
|
||||||
if (savedSizeString != null) {
|
// Get saved window size from preferences
|
||||||
try {
|
final savedSizeString = prefs.getString(kAppWindowSize);
|
||||||
final parts = savedSizeString.split(',');
|
Size initialSize = defaultSize;
|
||||||
if (parts.length == 2) {
|
|
||||||
final width = double.parse(parts[0]);
|
if (savedSizeString != null) {
|
||||||
final height = double.parse(parts[1]);
|
try {
|
||||||
initialSize = Size(width, height);
|
final parts = savedSizeString.split(',');
|
||||||
}
|
if (parts.length == 2) {
|
||||||
} catch (e) {
|
final width = double.parse(parts[0]);
|
||||||
log("[SplashScreen] Failed to parse saved window size: $e");
|
final height = double.parse(parts[1]);
|
||||||
initialSize = defaultSize;
|
initialSize = Size(width, height);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log("[SplashScreen] Failed to parse saved window size: $e");
|
||||||
|
initialSize = defaultSize;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
appWindow.minSize = defaultSize;
|
WindowOptions windowOptions = WindowOptions(
|
||||||
appWindow.size = initialSize;
|
size: initialSize,
|
||||||
appWindow.alignment = Alignment.center;
|
center: true,
|
||||||
appWindow.show();
|
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(
|
log(
|
||||||
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}",
|
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}",
|
||||||
);
|
);
|
||||||
@@ -165,6 +176,21 @@ class IslandApp extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = ref.watch(themeProvider);
|
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) {
|
void handleMessage(RemoteMessage notification) {
|
||||||
if (notification.data['meta']?['action_uri'] != null) {
|
if (notification.data['meta']?['action_uri'] != null) {
|
||||||
@@ -235,9 +261,10 @@ class IslandApp extends HookConsumerWidget {
|
|||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
|
color: Colors.transparent,
|
||||||
theme: theme?.light,
|
theme: theme?.light,
|
||||||
darkTheme: theme?.dark,
|
darkTheme: theme?.dark,
|
||||||
themeMode: ThemeMode.system,
|
themeMode: getThemeMode(),
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
scrollBehavior: AppScrollBehavior(),
|
scrollBehavior: AppScrollBehavior(),
|
||||||
|
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) => {});
|
220
lib/pods/chat/chat_subscribe.dart
Normal file
220
lib/pods/chat/chat_subscribe.dart
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
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},
|
||||||
|
endpoint: 'sphere',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} 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},
|
||||||
|
endpoint: 'sphere',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/websocket.dart';
|
||||||
|
|
||||||
part 'chat_summary.g.dart';
|
part 'chat_summary.g.dart';
|
||||||
|
|
||||||
@@ -12,9 +13,27 @@ class ChatSummary extends _$ChatSummary {
|
|||||||
final resp = await client.get('/sphere/chat/summary');
|
final resp = await client.get('/sphere/chat/summary');
|
||||||
|
|
||||||
final Map<String, dynamic> data = resp.data;
|
final Map<String, dynamic> data = resp.data;
|
||||||
return data.map(
|
final summaries = data.map(
|
||||||
(key, value) => MapEntry(key, SnChatSummary.fromJson(value)),
|
(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 {
|
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
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatSummaryHash() => r'87a10e4cefa37dc5fa8eadb175ef1b2bed6070bf';
|
String _$chatSummaryHash() => r'7b79dba7445f634373fbb2ee0ced99b2302097c2';
|
||||||
|
|
||||||
/// See also [ChatSummary].
|
/// See also [ChatSummary].
|
||||||
@ProviderFor(ChatSummary)
|
@ProviderFor(ChatSummary)
|
@@ -10,13 +10,14 @@ import "package:island/models/chat.dart";
|
|||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/pods/config.dart";
|
import "package:island/pods/config.dart";
|
||||||
import "package:island/pods/database.dart";
|
import "package:island/pods/database.dart";
|
||||||
|
import "package:island/pods/lifecycle.dart";
|
||||||
import "package:island/pods/network.dart";
|
import "package:island/pods/network.dart";
|
||||||
import "package:island/services/file.dart";
|
import "package:island/services/file.dart";
|
||||||
import "package:island/widgets/alert.dart";
|
import "package:island/widgets/alert.dart";
|
||||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||||
import "package:uuid/uuid.dart";
|
import "package:uuid/uuid.dart";
|
||||||
import "package:island/screens/chat/chat.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';
|
part 'messages_notifier.g.dart';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
bool _hasMore = true;
|
bool _hasMore = true;
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
bool _isJumping = false;
|
bool _isJumping = false;
|
||||||
|
DateTime? _lastPauseTime;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
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
|
// Only setup sync and lifecycle listeners if user is a member
|
||||||
if (identity != null) {
|
if (identity != null) {
|
||||||
ref.listen(appLifecycleStateProvider, (_, next) {
|
ref.listen(appLifecycleStateProvider, (_, next) {
|
||||||
if (next.hasValue && next.value == AppLifecycleState.resumed) {
|
next.whenData((state) {
|
||||||
developer.log(
|
if (state == AppLifecycleState.paused) {
|
||||||
'App resumed, syncing messages',
|
_lastPauseTime = DateTime.now();
|
||||||
name: 'MessagesNotifier',
|
developer.log(
|
||||||
);
|
'App paused, recording time',
|
||||||
syncMessages();
|
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]),
|
_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 {
|
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
||||||
@@ -622,8 +652,12 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
name: 'MessagesNotifier',
|
name: 'MessagesNotifier',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final targetId = remoteMessage.meta['message_id'] ?? remoteMessage.id;
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
remoteMessage,
|
remoteMessage.copyWith(
|
||||||
|
id: targetId,
|
||||||
|
meta: Map.of(remoteMessage.meta)..remove('message_id'),
|
||||||
|
),
|
||||||
MessageStatus.sent,
|
MessageStatus.sent,
|
||||||
);
|
);
|
||||||
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
|
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
|
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237';
|
String _$messagesNotifierHash() => r'4257c9b3792418e913d0bac3ef58e727314635af';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
@@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
import 'package:island/pods/theme.dart';
|
import 'package:island/pods/theme.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
part 'config.freezed.dart';
|
part 'config.freezed.dart';
|
||||||
part 'config.g.dart';
|
part 'config.g.dart';
|
||||||
@@ -24,9 +25,11 @@ const kAppDataSavingMode = 'app_data_saving_mode';
|
|||||||
const kAppSoundEffects = 'app_sound_effects';
|
const kAppSoundEffects = 'app_sound_effects';
|
||||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||||
const kAppWindowSize = 'app_window_size';
|
const kAppWindowSize = 'app_window_size';
|
||||||
|
const kAppWindowOpacity = 'app_window_opacity';
|
||||||
const kAppEnterToSend = 'app_enter_to_send';
|
const kAppEnterToSend = 'app_enter_to_send';
|
||||||
const kAppDefaultPoolId = 'app_default_pool_id';
|
const kAppDefaultPoolId = 'app_default_pool_id';
|
||||||
const kAppMessageDisplayStyle = 'app_message_display_style';
|
const kAppMessageDisplayStyle = 'app_message_display_style';
|
||||||
|
const kAppThemeMode = 'app_theme_mode';
|
||||||
const kFeaturedPostsCollapsedId =
|
const kFeaturedPostsCollapsedId =
|
||||||
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
'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 String? customFonts,
|
||||||
required int? appColorScheme, // The color stored via the int type
|
required int? appColorScheme, // The color stored via the int type
|
||||||
required Size? windowSize, // The window size for desktop platforms
|
required Size? windowSize, // The window size for desktop platforms
|
||||||
|
required double windowOpacity, // The window opacity for desktop platforms
|
||||||
required String? defaultPoolId,
|
required String? defaultPoolId,
|
||||||
required String messageDisplayStyle,
|
required String messageDisplayStyle,
|
||||||
|
required String? themeMode,
|
||||||
}) = _AppSettings;
|
}) = _AppSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +93,10 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
customFonts: prefs.getString(kAppCustomFonts),
|
customFonts: prefs.getString(kAppCustomFonts),
|
||||||
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
|
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
|
||||||
windowSize: _getWindowSizeFromPrefs(prefs),
|
windowSize: _getWindowSizeFromPrefs(prefs),
|
||||||
|
windowOpacity: prefs.getDouble(kAppWindowOpacity) ?? 1.0,
|
||||||
defaultPoolId: prefs.getString(kAppDefaultPoolId),
|
defaultPoolId: prefs.getString(kAppDefaultPoolId),
|
||||||
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
|
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
|
||||||
|
themeMode: prefs.getString(kAppThemeMode) ?? 'system',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +203,19 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
prefs.setString(kAppMessageDisplayStyle, value);
|
prefs.setString(kAppMessageDisplayStyle, value);
|
||||||
state = state.copyWith(messageDisplayStyle: 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 =
|
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
|
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
|
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
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -27,16 +28,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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
|
@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
|
@override
|
||||||
String toString() {
|
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;
|
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$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 AppSettings _self;
|
||||||
final $Res Function(AppSettings) _then;
|
final $Res Function(AppSettings) _then;
|
||||||
|
|
||||||
/// Create a copy of AppSettings
|
@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,}) {
|
||||||
/// 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,}) {
|
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
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
|
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 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 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 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?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
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) {
|
switch (_that) {
|
||||||
case _AppSettings() when $default != null:
|
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();
|
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) {
|
switch (_that) {
|
||||||
case _AppSettings():
|
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`
|
/// 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) {
|
switch (_that) {
|
||||||
case _AppSettings() when $default != null:
|
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;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
|||||||
|
|
||||||
|
|
||||||
class _AppSettings implements AppSettings {
|
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;
|
@override final bool autoTranslate;
|
||||||
@@ -228,8 +228,11 @@ class _AppSettings implements AppSettings {
|
|||||||
// The color stored via the int type
|
// The color stored via the int type
|
||||||
@override final Size? windowSize;
|
@override final Size? windowSize;
|
||||||
// The window size for desktop platforms
|
// The window size for desktop platforms
|
||||||
|
@override final double windowOpacity;
|
||||||
|
// The window opacity for desktop platforms
|
||||||
@override final String? defaultPoolId;
|
@override final String? defaultPoolId;
|
||||||
@override final String messageDisplayStyle;
|
@override final String messageDisplayStyle;
|
||||||
|
@override final String? themeMode;
|
||||||
|
|
||||||
/// Create a copy of AppSettings
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -241,16 +244,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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
|
@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
|
@override
|
||||||
String toString() {
|
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;
|
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$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
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// 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(
|
return _then(_AppSettings(
|
||||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
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
|
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 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 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 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?,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,
|
as String,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ part of 'config.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$appSettingsNotifierHash() =>
|
String _$appSettingsNotifierHash() =>
|
||||||
r'9f0979f18b107e61185391e7c39bd81ac4b8ca50';
|
r'3b0967a39a375c664c3fd44cbee7936b8b2f5fec';
|
||||||
|
|
||||||
/// See also [AppSettingsNotifier].
|
/// See also [AppSettingsNotifier].
|
||||||
@ProviderFor(AppSettingsNotifier)
|
@ProviderFor(AppSettingsNotifier)
|
||||||
|
@@ -2,10 +2,6 @@ import "dart:async";
|
|||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.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 appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||||
final controller = StreamController<AppLifecycleState>();
|
final controller = StreamController<AppLifecycleState>();
|
||||||
|
|
@@ -211,21 +211,11 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
|||||||
icon: Symbols.system_update,
|
icon: Symbols.system_update,
|
||||||
title: 'Check for updates',
|
title: 'Check for updates',
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
// Fetch latest release and show the unified sheet
|
|
||||||
final svc = UpdateService();
|
final svc = UpdateService();
|
||||||
// Reuse service fetch + compare to decide content
|
|
||||||
showLoadingModal(context);
|
showLoadingModal(context);
|
||||||
final release = await svc.fetchLatestRelease();
|
svc.checkForUpdates(context);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
hideLoadingModal(context);
|
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(
|
_buildListTile(
|
||||||
|
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart' hide ConnectionState;
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
import 'package:island/widgets/chat/call_overlay.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/chat.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/realm.dart';
|
import 'package:island/models/realm.dart';
|
||||||
import 'package:island/pods/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
import 'package:island/pods/chat_summary.dart';
|
import 'package:island/pods/chat/chat_summary.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/realm/realms.dart';
|
import 'package:island/screens/realm/realms.dart';
|
||||||
@@ -178,6 +178,102 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
|||||||
.toList();
|
.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 {
|
class ChatShellScreen extends HookConsumerWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const ChatShellScreen({super.key, required this.child});
|
const ChatShellScreen({super.key, required this.child});
|
||||||
@@ -191,9 +287,23 @@ class ChatShellScreen extends HookConsumerWidget {
|
|||||||
isRoot: true,
|
isRoot: true,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(flex: 2, child: ChatListScreen(isAside: true)),
|
Flexible(
|
||||||
const VerticalDivider(width: 1),
|
flex: 2,
|
||||||
Flexible(flex: 4, child: child),
|
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 {
|
class ChatListScreen extends HookConsumerWidget {
|
||||||
final bool isAside;
|
final bool isAside;
|
||||||
const ChatListScreen({super.key, this.isAside = false});
|
final bool isFloating;
|
||||||
|
const ChatListScreen({
|
||||||
|
super.key,
|
||||||
|
this.isAside = false,
|
||||||
|
this.isFloating = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isWide = isWideScreen(context);
|
final isWide = isWideScreen(context);
|
||||||
if (isWide && !isAside) {
|
|
||||||
return const EmptyPageHolder();
|
|
||||||
}
|
|
||||||
|
|
||||||
final chats = ref.watch(chatroomsJoinedProvider);
|
|
||||||
final chatInvites = ref.watch(chatroomInvitesProvider);
|
final chatInvites = ref.watch(chatroomInvitesProvider);
|
||||||
final tabController = useTabController(initialLength: 3);
|
final tabController = useTabController(initialLength: 3);
|
||||||
final selectedTab = useState(
|
final selectedTab = useState(
|
||||||
0,
|
0,
|
||||||
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
|
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
|
||||||
|
|
||||||
final callState = ref.watch(callNotifierProvider);
|
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
tabController.addListener(() {
|
tabController.addListener(() {
|
||||||
selectedTab.value = tabController.index;
|
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(
|
return AppScaffold(
|
||||||
extendBody: false, // Prevent conflicts with tabs navigation
|
extendBody: false, // Prevent conflicts with tabs navigation
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -353,81 +532,10 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
child: const Icon(Symbols.add),
|
child: const Icon(Symbols.add),
|
||||||
),
|
),
|
||||||
floatingActionButtonLocation: TabbedFabLocation(context),
|
floatingActionButtonLocation: TabbedFabLocation(context),
|
||||||
body: Column(
|
body: ChatListBodyWidget(
|
||||||
children: [
|
isFloating: false,
|
||||||
Consumer(
|
tabController: tabController,
|
||||||
builder: (context, ref, _) {
|
selectedTab: selectedTab,
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ import "package:island/widgets/chat/message_item.dart";
|
|||||||
import "package:island/widgets/response.dart";
|
import "package:island/widgets/response.dart";
|
||||||
import "package:island/pods/network.dart";
|
import "package:island/pods/network.dart";
|
||||||
import "package:island/services/responsive.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 {
|
class PublicRoomPreview extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
import "dart:async";
|
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:easy_localization/easy_localization.dart";
|
||||||
import "package:file_picker/file_picker.dart";
|
import "package:file_picker/file_picker.dart";
|
||||||
import "package:flutter/material.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/database/message.dart";
|
||||||
import "package:island/models/chat.dart";
|
import "package:island/models/chat.dart";
|
||||||
import "package:island/models/file.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/config.dart";
|
||||||
import "package:island/pods/file_pool.dart";
|
import "package:island/pods/chat/messages_notifier.dart";
|
||||||
import "package:island/pods/messages_notifier.dart";
|
|
||||||
import "package:island/pods/network.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/services/file.dart";
|
||||||
import "package:island/screens/chat/chat.dart";
|
import "package:island/screens/chat/chat.dart";
|
||||||
import "package:island/services/responsive.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/attachment_uploader.dart";
|
||||||
import "package:island/widgets/chat/call_overlay.dart";
|
import "package:island/widgets/chat/call_overlay.dart";
|
||||||
import "package:island/widgets/chat/message_item.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/cloud_files.dart";
|
||||||
import "package:island/widgets/content/sheet.dart";
|
|
||||||
import "package:island/widgets/post/compose_shared.dart";
|
import "package:island/widgets/post/compose_shared.dart";
|
||||||
import "package:island/widgets/response.dart";
|
import "package:island/widgets/response.dart";
|
||||||
import "package:material_symbols_icons/material_symbols_icons.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/chat_input.dart";
|
||||||
import "package:island/widgets/chat/public_room_preview.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 {
|
class ChatRoomScreen extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
const ChatRoomScreen({super.key, required this.id});
|
const ChatRoomScreen({super.key, required this.id});
|
||||||
@@ -79,6 +43,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final chatRoom = ref.watch(chatroomProvider(id));
|
final chatRoom = ref.watch(chatroomProvider(id));
|
||||||
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
|
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
|
||||||
final isSyncing = ref.watch(isSyncingProvider);
|
final isSyncing = ref.watch(isSyncingProvider);
|
||||||
|
final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id));
|
||||||
|
|
||||||
|
final hasOnlineCount = onlineCount.hasValue;
|
||||||
|
|
||||||
if (chatIdentity.isLoading || chatRoom.isLoading) {
|
if (chatIdentity.isLoading || chatRoom.isLoading) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
@@ -163,7 +130,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final messages = ref.watch(messagesNotifierProvider(id));
|
final messages = ref.watch(messagesNotifierProvider(id));
|
||||||
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
||||||
final ws = ref.watch(websocketProvider);
|
final chatSubscribeNotifier = ref.read(
|
||||||
|
chatSubscribeNotifierProvider(id).notifier,
|
||||||
|
);
|
||||||
|
|
||||||
final messageController = useTextEditingController();
|
final messageController = useTextEditingController();
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
@@ -174,65 +143,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final attachments = useState<List<UniversalFile>>([]);
|
final attachments = useState<List<UniversalFile>>([]);
|
||||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
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;
|
var isLoading = false;
|
||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
@@ -252,85 +162,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
return () => scrollController.removeListener(onScroll);
|
return () => scrollController.removeListener(onScroll);
|
||||||
}, [scrollController]);
|
}, [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 {
|
Future<void> pickPhotoMedia() async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.image,
|
type: FileType.image,
|
||||||
@@ -364,21 +195,19 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
void sendMessage() {
|
void sendMessage() {
|
||||||
if (messageController.text.trim().isNotEmpty ||
|
if (messageController.text.trim().isNotEmpty ||
|
||||||
attachments.value.isNotEmpty) {
|
attachments.value.isNotEmpty) {
|
||||||
messagesNotifier
|
messagesNotifier.sendMessage(
|
||||||
.sendMessage(
|
messageController.text.trim(),
|
||||||
messageController.text.trim(),
|
attachments.value,
|
||||||
attachments.value,
|
editingTo: messageEditingTo.value,
|
||||||
editingTo: messageEditingTo.value,
|
forwardingTo: messageForwardingTo.value,
|
||||||
forwardingTo: messageForwardingTo.value,
|
replyingTo: messageReplyingTo.value,
|
||||||
replyingTo: messageReplyingTo.value,
|
onProgress: (messageId, progress) {
|
||||||
onProgress: (messageId, progress) {
|
attachmentProgress.value = {
|
||||||
attachmentProgress.value = {
|
...attachmentProgress.value,
|
||||||
...attachmentProgress.value,
|
messageId: progress,
|
||||||
messageId: progress,
|
};
|
||||||
};
|
},
|
||||||
},
|
);
|
||||||
)
|
|
||||||
.then((_) => sendReadReceipt());
|
|
||||||
messageController.clear();
|
messageController.clear();
|
||||||
messageEditingTo.value = null;
|
messageEditingTo.value = null;
|
||||||
messageReplyingTo.value = null;
|
messageReplyingTo.value = null;
|
||||||
@@ -391,7 +220,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
useEffect(() {
|
useEffect(() {
|
||||||
void onTextChange() {
|
void onTextChange() {
|
||||||
if (messageController.text.isNotEmpty) {
|
if (messageController.text.isNotEmpty) {
|
||||||
sendTypingStatus();
|
chatSubscribeNotifier.sendTypingStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +230,32 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final compactHeader = isWideScreen(context);
|
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(
|
Widget comfortHeaderWidget(SnChatRoom? room) => Column(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -434,16 +289,18 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
? room.members!.map((e) => e.account.nick).join(', ')
|
? room.members!.map((e) => e.account.nick).join(', ')
|
||||||
: room.name!,
|
: room.name!,
|
||||||
).fontSize(15),
|
).fontSize(15),
|
||||||
|
if (hasOnlineCount) onlineIndicator(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget compactHeaderWidget(SnChatRoom? room) => Row(
|
Widget compactHeaderWidget(SnChatRoom? room) => Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 26,
|
height: 28,
|
||||||
width: 26,
|
width: 28,
|
||||||
child:
|
child:
|
||||||
(room!.type == 1 && room.picture?.id == null)
|
(room!.type == 1 && room.picture?.id == null)
|
||||||
? SplitAvatarWidget(
|
? SplitAvatarWidget(
|
||||||
@@ -469,6 +326,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
? room.members!.map((e) => e.account.nick).join(', ')
|
? room.members!.map((e) => e.account.nick).join(', ')
|
||||||
: room.name!,
|
: room.name!,
|
||||||
).fontSize(19),
|
).fontSize(19),
|
||||||
|
if (hasOnlineCount) onlineIndicator().padding(left: 4, top: 6),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -482,7 +340,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder:
|
builder:
|
||||||
(context) => ChatAttachmentUploaderSheet(
|
(context) => AttachmentUploaderSheet(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
attachments: attachments.value,
|
attachments: attachments.value,
|
||||||
index: index,
|
index: index,
|
||||||
@@ -541,7 +399,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||||
SuperListView.builder(
|
SuperListView.builder(
|
||||||
listController: listController,
|
listController: listController,
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: EdgeInsets.only(
|
||||||
|
top: 16,
|
||||||
|
bottom: 80 + MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
reverse: true, // Show newest messages at the bottom
|
reverse: true, // Show newest messages at the bottom
|
||||||
itemCount: messageList.length,
|
itemCount: messageList.length,
|
||||||
@@ -655,7 +516,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
toolbarHeight: compactHeader ? null : 64,
|
toolbarHeight: compactHeader ? null : 80,
|
||||||
title: chatRoom.when(
|
title: chatRoom.when(
|
||||||
data:
|
data:
|
||||||
(room) =>
|
(room) =>
|
||||||
@@ -723,169 +584,92 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
bottom:
|
|
||||||
isSyncing
|
|
||||||
? const PreferredSize(
|
|
||||||
preferredSize: Size.fromHeight(2),
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
borderRadius: BorderRadius.zero,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
// Messages
|
||||||
children: [
|
Positioned.fill(
|
||||||
Expanded(
|
child: messages.when(
|
||||||
child: messages.when(
|
data:
|
||||||
data:
|
(messageList) =>
|
||||||
(messageList) =>
|
messageList.isEmpty
|
||||||
messageList.isEmpty
|
? Center(child: Text('No messages yet'.tr()))
|
||||||
? Center(child: Text('No messages yet'.tr()))
|
: chatMessageListWidget(messageList),
|
||||||
: chatMessageListWidget(messageList),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
loading:
|
error:
|
||||||
() => const Center(child: CircularProgressIndicator()),
|
(error, _) => ResponseErrorWidget(
|
||||||
error:
|
error: error,
|
||||||
(error, _) => ResponseErrorWidget(
|
onRetry: () => messagesNotifier.loadInitial(),
|
||||||
error: error,
|
),
|
||||||
onRetry: () => messagesNotifier.loadInitial(),
|
),
|
||||||
),
|
),
|
||||||
),
|
// Input
|
||||||
),
|
Positioned(
|
||||||
chatRoom.when(
|
bottom: 0,
|
||||||
data:
|
left: 0,
|
||||||
(room) => Column(
|
right: 0,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: chatRoom.when(
|
||||||
children: [
|
data:
|
||||||
AnimatedSwitcher(
|
(room) => Column(
|
||||||
duration: const Duration(milliseconds: 150),
|
mainAxisSize: MainAxisSize.min,
|
||||||
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
children: [
|
||||||
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
ChatInput(
|
||||||
transitionBuilder: (
|
messageController: messageController,
|
||||||
Widget child,
|
chatRoom: room!,
|
||||||
Animation<double> animation,
|
onSend: sendMessage,
|
||||||
) {
|
onClear: () {
|
||||||
return SlideTransition(
|
if (messageEditingTo.value != null) {
|
||||||
position: Tween<Offset>(
|
attachments.value.clear();
|
||||||
begin: const Offset(0, -0.3),
|
messageController.clear();
|
||||||
end: Offset.zero,
|
}
|
||||||
).animate(
|
messageEditingTo.value = null;
|
||||||
CurvedAnimation(
|
messageReplyingTo.value = null;
|
||||||
parent: animation,
|
messageForwardingTo.value = null;
|
||||||
curve: Curves.easeOutCubic,
|
},
|
||||||
),
|
messageEditingTo: messageEditingTo.value,
|
||||||
),
|
messageReplyingTo: messageReplyingTo.value,
|
||||||
child: SizeTransition(
|
messageForwardingTo: messageForwardingTo.value,
|
||||||
sizeFactor: animation,
|
onPickFile: (bool isPhoto) {
|
||||||
axisAlignment: -1.0,
|
if (isPhoto) {
|
||||||
child: FadeTransition(
|
pickPhotoMedia();
|
||||||
opacity: animation,
|
} else {
|
||||||
child: child,
|
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:
|
final clone = List.of(attachments.value);
|
||||||
typingStatuses.value.isNotEmpty
|
clone.removeAt(index);
|
||||||
? Container(
|
attachments.value = clone;
|
||||||
key: const ValueKey('typing-indicator'),
|
},
|
||||||
width: double.infinity,
|
onMoveAttachment: (idx, delta) {
|
||||||
padding: const EdgeInsets.symmetric(
|
if (idx + delta < 0 ||
|
||||||
horizontal: 16,
|
idx + delta >= attachments.value.length) {
|
||||||
vertical: 4,
|
return;
|
||||||
),
|
}
|
||||||
child: Row(
|
final clone = List.of(attachments.value);
|
||||||
children: [
|
clone.insert(idx + delta, clone.removeAt(idx));
|
||||||
const Icon(
|
attachments.value = clone;
|
||||||
Symbols.more_horiz,
|
},
|
||||||
size: 16,
|
onAttachmentsChanged: (newAttachments) {
|
||||||
).padding(horizontal: 8),
|
attachments.value = newAttachments;
|
||||||
const Gap(8),
|
},
|
||||||
Expanded(
|
attachmentProgress: attachmentProgress.value,
|
||||||
child: Text(
|
),
|
||||||
'typingHint'.plural(
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
typingStatuses.value.length,
|
],
|
||||||
args: [
|
),
|
||||||
typingStatuses.value
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
.map(
|
loading: () => const SizedBox.shrink(),
|
||||||
(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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -893,347 +677,37 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
top: 0,
|
top: 0,
|
||||||
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
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!;
|
|
||||||
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.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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
@@ -12,6 +12,7 @@ import 'package:island/screens/posts/compose.dart';
|
|||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/extended_refresh_indicator.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_item.dart';
|
||||||
import 'package:island/widgets/post/post_award_history_sheet.dart';
|
import 'package:island/widgets/post/post_award_history_sheet.dart';
|
||||||
import 'package:island/widgets/post/post_pin_sheet.dart';
|
import 'package:island/widgets/post/post_pin_sheet.dart';
|
||||||
@@ -273,7 +274,14 @@ class PostActionButtons extends HookConsumerWidget {
|
|||||||
|
|
||||||
actions.add(
|
actions.add(
|
||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (context) => PostAwardSheet(post: post),
|
||||||
|
);
|
||||||
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
@@ -92,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
|
// Custom fonts settings
|
||||||
ListTile(
|
ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
@@ -579,8 +621,33 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Desktop-specific settings
|
// Desktop-specific settings
|
||||||
// But nothing for now
|
final desktopSettings =
|
||||||
final desktopSettings = !isDesktop ? <Widget>[] : <Widget>[];
|
!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
|
// Create a responsive layout based on screen width
|
||||||
Widget buildSettingsList() {
|
Widget buildSettingsList() {
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class TrayService {
|
class TrayService {
|
||||||
TrayService._();
|
TrayService._();
|
||||||
@@ -48,15 +47,10 @@ class TrayService {
|
|||||||
void handleAction(MenuItem item) {
|
void handleAction(MenuItem item) {
|
||||||
switch (item.key) {
|
switch (item.key) {
|
||||||
case 'show_window':
|
case 'show_window':
|
||||||
() async {
|
windowManager.show();
|
||||||
appWindow.show();
|
|
||||||
appWindow.restore();
|
|
||||||
await Future.delayed(const Duration(milliseconds: 32));
|
|
||||||
appWindow.show();
|
|
||||||
}();
|
|
||||||
break;
|
break;
|
||||||
case 'exit_app':
|
case 'exit_app':
|
||||||
appWindow.close();
|
windowManager.destroy();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_app_update/azhon_app_update.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';
|
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
|
/// 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 {
|
try {
|
||||||
log('[Update] Starting Windows installer download from: $url');
|
log('[Update] Starting Windows installer download from: $url');
|
||||||
|
|
||||||
@@ -243,7 +444,7 @@ class UpdateService {
|
|||||||
'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip';
|
'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip';
|
||||||
final filePath = path.join(tempDir.path, fileName);
|
final filePath = path.join(tempDir.path, fileName);
|
||||||
|
|
||||||
final response = await _dio.download(
|
final response = await Dio().download(
|
||||||
url,
|
url,
|
||||||
filePath,
|
filePath,
|
||||||
onReceiveProgress: (received, total) {
|
onReceiveProgress: (received, total) {
|
||||||
@@ -252,6 +453,7 @@ class UpdateService {
|
|||||||
'[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%',
|
'[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
onProgress?.call(received, total);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -311,13 +513,20 @@ class UpdateService {
|
|||||||
try {
|
try {
|
||||||
log('[Update] Running Windows installer from: $extractDir');
|
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()) {
|
if (exeFiles.isEmpty) {
|
||||||
log('[Update] setup.exe not found in extracted directory');
|
log('[Update] No .exe file found in extracted directory');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final setupExePath = exeFiles.first.path;
|
||||||
|
log('[Update] Found installer executable: $setupExePath');
|
||||||
|
|
||||||
final shell = Shell();
|
final shell = Shell();
|
||||||
final results = await shell.run(setupExePath);
|
final results = await shell.run(setupExePath);
|
||||||
final result = results.first;
|
final result = results.first;
|
||||||
@@ -338,202 +547,6 @@ class UpdateService {
|
|||||||
return false;
|
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 {
|
class _UpdateSheet extends StatefulWidget {
|
||||||
@@ -584,7 +597,7 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: 'Update available',
|
titleText: 'updateAvailable'.tr(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||||
@@ -612,14 +625,14 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
|||||||
child: MarkdownTextContent(
|
child: MarkdownTextContent(
|
||||||
content:
|
content:
|
||||||
widget.release.body.isEmpty
|
widget.release.body.isEmpty
|
||||||
? 'No changelog provided.'
|
? 'noChangelogProvided'.tr()
|
||||||
: widget.release.body,
|
: widget.release.body,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!kIsWeb && Platform.isAndroid)
|
if (!kIsWeb && Platform.isAndroid)
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('Use secondary source for download'),
|
title: Text('useSecondarySourceForDownload'.tr()),
|
||||||
value: _useProxy,
|
value: _useProxy,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -642,7 +655,7 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
|||||||
_installUpdate(widget.androidUpdateUrl!);
|
_installUpdate(widget.androidUpdateUrl!);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.update),
|
icon: const Icon(Symbols.update),
|
||||||
label: const Text('Install update'),
|
label: Text('installUpdate'.tr()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!kIsWeb &&
|
if (!kIsWeb &&
|
||||||
@@ -655,20 +668,20 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
|||||||
final updateService = UpdateService(
|
final updateService = UpdateService(
|
||||||
useProxy: widget.useProxy,
|
useProxy: widget.useProxy,
|
||||||
);
|
);
|
||||||
updateService._performAutomaticWindowsUpdate(
|
updateService.performAutomaticWindowsUpdate(
|
||||||
context,
|
context,
|
||||||
widget.windowsUpdateUrl!,
|
widget.windowsUpdateUrl!,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.update),
|
icon: const Icon(Symbols.update),
|
||||||
label: const Text('Install update'),
|
label: Text('installUpdate'.tr()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: widget.onOpen,
|
onPressed: widget.onOpen,
|
||||||
icon: const Icon(Icons.open_in_new),
|
icon: const Icon(Icons.open_in_new),
|
||||||
label: const Text('Open release page'),
|
label: Text('openReleasePage'.tr()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@@ -6,7 +6,7 @@ part of 'status.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$accountStatusHash() => r'c861a0565d6229fd35666bba7cb2f5c6b7298e46';
|
String _$accountStatusHash() => r'abc2f11f0fbaf637efc182cf85ab838936c4d875';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.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:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class AppScrollBehavior extends MaterialScrollBehavior {
|
class AppScrollBehavior extends MaterialScrollBehavior {
|
||||||
@override
|
@override
|
||||||
@@ -31,16 +31,19 @@ class WindowScaffold extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isMaximized = useState(false);
|
||||||
|
|
||||||
// Add window resize listener for desktop platforms
|
// Add window resize listener for desktop platforms
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (!kIsWeb &&
|
if (!kIsWeb &&
|
||||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
void saveWindowSize() {
|
void saveWindowSize() {
|
||||||
final size = appWindow.size;
|
windowManager.getBounds().then((bounds) {
|
||||||
final settingsNotifier = ref.read(
|
final settingsNotifier = ref.read(
|
||||||
appSettingsNotifierProvider.notifier,
|
appSettingsNotifierProvider.notifier,
|
||||||
);
|
);
|
||||||
settingsNotifier.setWindowSize(size);
|
settingsNotifier.setWindowSize(bounds.size);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save window size when app is about to close
|
// Save window size when app is about to close
|
||||||
@@ -48,11 +51,16 @@ class WindowScaffold extends HookConsumerWidget {
|
|||||||
_WindowSizeObserver(saveWindowSize),
|
_WindowSizeObserver(saveWindowSize),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final maximizeListener = _WindowMaximizeListener(isMaximized);
|
||||||
|
windowManager.addListener(maximizeListener);
|
||||||
|
windowManager.isMaximized().then((max) => isMaximized.value = max);
|
||||||
|
|
||||||
return () {
|
return () {
|
||||||
// Cleanup observer when widget is disposed
|
// Cleanup observer when widget is disposed
|
||||||
WidgetsBinding.instance.removeObserver(
|
WidgetsBinding.instance.removeObserver(
|
||||||
_WindowSizeObserver(saveWindowSize),
|
_WindowSizeObserver(saveWindowSize),
|
||||||
);
|
);
|
||||||
|
windowManager.removeListener(maximizeListener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -61,13 +69,6 @@ class WindowScaffold extends HookConsumerWidget {
|
|||||||
if (!kIsWeb &&
|
if (!kIsWeb &&
|
||||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
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(
|
return Material(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -75,44 +76,80 @@ class WindowScaffold extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
WindowTitleBarBox(
|
Container(
|
||||||
child: Container(
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
border: Border(
|
||||||
border: Border(
|
bottom: BorderSide(
|
||||||
bottom: BorderSide(
|
color: Theme.of(context).dividerColor,
|
||||||
color: Theme.of(context).dividerColor,
|
width: 1 / devicePixelRatio,
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: MoveWindow(
|
),
|
||||||
child: Row(
|
child: DragToMoveArea(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Row(
|
||||||
mainAxisAlignment:
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Platform.isMacOS
|
mainAxisAlignment:
|
||||||
? MainAxisAlignment.center
|
Platform.isMacOS
|
||||||
: MainAxisAlignment.start,
|
? MainAxisAlignment.center
|
||||||
children: [
|
: MainAxisAlignment.start,
|
||||||
Expanded(
|
children: [
|
||||||
child: Text(
|
Expanded(
|
||||||
'Solar Network',
|
child: Platform.isMacOS
|
||||||
textAlign:
|
? Text(
|
||||||
Platform.isMacOS
|
'Solar Network',
|
||||||
? TextAlign.center
|
textAlign: TextAlign.center,
|
||||||
: TextAlign.start,
|
).padding(horizontal: 12, vertical: 5)
|
||||||
).padding(horizontal: 12, vertical: 5),
|
: Row(
|
||||||
),
|
children: [
|
||||||
if (!Platform.isMacOS)
|
Image.asset(
|
||||||
MinimizeWindowButton(colors: windowButtonColor),
|
Theme.of(context).brightness == Brightness.dark
|
||||||
if (!Platform.isMacOS)
|
? 'assets/icons/icon-dark.png'
|
||||||
MaximizeWindowButton(colors: windowButtonColor),
|
: 'assets/icons/icon.png',
|
||||||
if (!Platform.isMacOS)
|
width: 20,
|
||||||
CloseWindowButton(
|
height: 20,
|
||||||
colors: windowButtonColor,
|
),
|
||||||
onPressed: () => appWindow.hide(),
|
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;
|
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>();
|
final rootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
class AppScaffold extends HookConsumerWidget {
|
class AppScaffold extends HookConsumerWidget {
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/content/network_status_sheet.dart';
|
||||||
import 'package:island/widgets/tour/tour.dart';
|
import 'package:island/widgets/tour/tour.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class AppWrapper extends HookConsumerWidget with TrayListener {
|
class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@@ -67,11 +67,7 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _trayIconPrimaryAction() {
|
void _trayIconPrimaryAction() {
|
||||||
if (appWindow.isVisible) {
|
windowManager.show();
|
||||||
appWindow.restore();
|
|
||||||
} else {
|
|
||||||
appWindow.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _trayIconSecondaryAction() {
|
void _trayIconSecondaryAction() {
|
||||||
|
@@ -26,15 +26,20 @@ class AttachmentUploadConfig {
|
|||||||
|
|
||||||
class AttachmentUploaderSheet extends StatefulWidget {
|
class AttachmentUploaderSheet extends StatefulWidget {
|
||||||
final WidgetRef ref;
|
final WidgetRef ref;
|
||||||
final ComposeState state;
|
final ComposeState? state;
|
||||||
|
final List<UniversalFile>? attachments;
|
||||||
final int index;
|
final int index;
|
||||||
|
|
||||||
const AttachmentUploaderSheet({
|
const AttachmentUploaderSheet({
|
||||||
super.key,
|
super.key,
|
||||||
required this.ref,
|
required this.ref,
|
||||||
required this.state,
|
this.state,
|
||||||
|
this.attachments,
|
||||||
required this.index,
|
required this.index,
|
||||||
});
|
}) : assert(
|
||||||
|
state != null || attachments != null,
|
||||||
|
'Either state or attachments must be provided',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AttachmentUploaderSheet> createState() =>
|
State<AttachmentUploaderSheet> createState() =>
|
||||||
@@ -46,7 +51,9 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return SheetScaffold(
|
||||||
titleText: 'uploadAttachment'.tr(),
|
titleText: 'uploadAttachment'.tr(),
|
||||||
@@ -111,19 +118,18 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
|||||||
|
|
||||||
// Check accepted types
|
// Check accepted types
|
||||||
final acceptTypes =
|
final acceptTypes =
|
||||||
selectedPool.policyConfig?['accept_types']
|
(selectedPool.policyConfig?['accept_types']
|
||||||
as List?;
|
as List?)
|
||||||
|
?.cast<String>();
|
||||||
final mimeType =
|
final mimeType =
|
||||||
attachment.data.mimeType ??
|
attachment.data.mimeType ??
|
||||||
ComposeLogic.getMimeTypeFromFileType(
|
ComposeLogic.getMimeTypeFromFileType(
|
||||||
attachment.type,
|
attachment.type,
|
||||||
);
|
);
|
||||||
final typeAccepted =
|
final typeAccepted = _isMimeTypeAccepted(
|
||||||
acceptTypes == null ||
|
mimeType,
|
||||||
acceptTypes.isEmpty ||
|
acceptTypes,
|
||||||
acceptTypes.any(
|
);
|
||||||
(type) => mimeType.startsWith(type),
|
|
||||||
);
|
|
||||||
|
|
||||||
final hasIssues = fileSizeExceeded || !typeAccepted;
|
final hasIssues = fileSizeExceeded || !typeAccepted;
|
||||||
|
|
||||||
@@ -279,7 +285,9 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<AttachmentUploadConfig?> _getUploadConfig() async {
|
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);
|
final fileSize = await _getFileSize(attachment);
|
||||||
|
|
||||||
if (fileSize == null) return null;
|
if (fileSize == null) return null;
|
||||||
@@ -292,14 +300,12 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
|||||||
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
|
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
|
||||||
final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize;
|
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 =
|
final mimeType =
|
||||||
attachment.data.mimeType ??
|
attachment.data.mimeType ??
|
||||||
ComposeLogic.getMimeTypeFromFileType(attachment.type);
|
ComposeLogic.getMimeTypeFromFileType(attachment.type);
|
||||||
final typeAccepted =
|
final typeAccepted = _isMimeTypeAccepted(mimeType, acceptTypes);
|
||||||
acceptTypes == null ||
|
|
||||||
acceptTypes.isEmpty ||
|
|
||||||
acceptTypes.any((type) => mimeType.startsWith(type));
|
|
||||||
|
|
||||||
final hasConstraints = fileSizeExceeded || !typeAccepted;
|
final hasConstraints = fileSizeExceeded || !typeAccepted;
|
||||||
|
|
||||||
@@ -360,4 +366,16 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
|||||||
final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round();
|
final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round();
|
||||||
return _formatNumber(quotaCost);
|
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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/chat.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/pods/network.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.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:go_router/go_router.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/pods/network.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.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:flutter_popup_card/flutter_popup_card.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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:island/widgets/account/account_nameplate.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/screens/account/profile.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_card.dart';
|
import 'package:island/widgets/chat/call_participant_card.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
import "dart:async";
|
import "dart:async";
|
||||||
import "dart:io";
|
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:flutter/foundation.dart";
|
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.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/chat.dart";
|
||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/pods/config.dart";
|
import "package:island/pods/config.dart";
|
||||||
|
import "package:island/services/responsive.dart";
|
||||||
import "package:island/widgets/content/attachment_preview.dart";
|
import "package:island/widgets/content/attachment_preview.dart";
|
||||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||||
import "package:pasteboard/pasteboard.dart";
|
import "package:pasteboard/pasteboard.dart";
|
||||||
import "package:styled_widget/styled_widget.dart";
|
import "package:styled_widget/styled_widget.dart";
|
||||||
import "package:material_symbols_icons/symbols.dart";
|
import "package:material_symbols_icons/symbols.dart";
|
||||||
import "package:island/widgets/stickers/picker.dart";
|
import "package:island/widgets/stickers/picker.dart";
|
||||||
|
import "package:island/pods/chat/chat_subscribe.dart";
|
||||||
|
|
||||||
class ChatInput extends HookConsumerWidget {
|
class ChatInput extends HookConsumerWidget {
|
||||||
final TextEditingController messageController;
|
final TextEditingController messageController;
|
||||||
@@ -55,10 +55,7 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final inputFocusNode = useFocusNode();
|
final inputFocusNode = useFocusNode();
|
||||||
|
final chatSubscribe = ref.watch(chatSubscribeNotifierProvider(chatRoom.id));
|
||||||
final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend;
|
|
||||||
|
|
||||||
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
|
||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
onSend.call();
|
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 {
|
Future<void> handlePaste() async {
|
||||||
final clipboard = await Pasteboard.image;
|
final clipboard = await Pasteboard.image;
|
||||||
if (clipboard == null) return;
|
if (clipboard == null) return;
|
||||||
@@ -80,212 +89,269 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleKeyPress(
|
inputFocusNode.onKeyEvent = (node, event) {
|
||||||
BuildContext context,
|
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||||
WidgetRef ref,
|
|
||||||
RawKeyEvent event,
|
|
||||||
) {
|
|
||||||
if (event is! RawKeyDownEvent) return;
|
|
||||||
|
|
||||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||||
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
final isModifierPressed =
|
||||||
|
HardwareKeyboard.instance.isMetaPressed ||
|
||||||
|
HardwareKeyboard.instance.isControlPressed;
|
||||||
|
|
||||||
if (isPaste && isModifierPressed) {
|
if (isPaste && isModifierPressed) {
|
||||||
handlePaste();
|
handlePaste();
|
||||||
return;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
|
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
|
||||||
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
|
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
|
||||||
|
|
||||||
if (isEnter) {
|
if (isEnter) {
|
||||||
if (enterToSend && !isModifierPressed) {
|
if (isModifierPressed) {
|
||||||
send();
|
insertNewLine();
|
||||||
} else if (!enterToSend && isModifierPressed) {
|
return KeyEventResult.handled;
|
||||||
|
} else if (enterToSend) {
|
||||||
send();
|
send();
|
||||||
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Material(
|
return KeyEventResult.ignored;
|
||||||
elevation: 8,
|
};
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Column(
|
final double leftMargin = isWideScreen(context) ? 8 : 16;
|
||||||
children: [
|
final double rightMargin = isWideScreen(context) ? leftMargin + 8 : 16;
|
||||||
if (attachments.isNotEmpty)
|
const double bottomMargin = 16;
|
||||||
SizedBox(
|
|
||||||
height: 280,
|
return Container(
|
||||||
child: ListView.separated(
|
margin: EdgeInsets.only(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
left: leftMargin,
|
||||||
scrollDirection: Axis.horizontal,
|
right: rightMargin,
|
||||||
itemCount: attachments.length,
|
bottom: bottomMargin,
|
||||||
itemBuilder: (context, idx) {
|
),
|
||||||
return SizedBox(
|
child: Material(
|
||||||
height: 280,
|
elevation: 2,
|
||||||
width: 280,
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
child: AttachmentPreview(
|
borderRadius: BorderRadius.circular(32),
|
||||||
item: attachments[idx],
|
child: Padding(
|
||||||
progress: attachmentProgress['chat-upload']?[idx],
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
onRequestUpload: () => onUploadAttachment(idx),
|
child: Column(
|
||||||
onDelete: () => onDeleteAttachment(idx),
|
children: [
|
||||||
onUpdate: (value) {
|
AnimatedSwitcher(
|
||||||
attachments[idx] = value;
|
duration: const Duration(milliseconds: 150),
|
||||||
onAttachmentsChanged(attachments);
|
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
||||||
},
|
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
||||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
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),
|
child:
|
||||||
),
|
chatSubscribe.isNotEmpty
|
||||||
).padding(top: 12),
|
? Container(
|
||||||
if (messageReplyingTo != null ||
|
key: const ValueKey('typing-indicator'),
|
||||||
messageForwardingTo != null ||
|
width: double.infinity,
|
||||||
messageEditingTo != null)
|
padding: const EdgeInsets.symmetric(
|
||||||
Container(
|
horizontal: 12,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
vertical: 4,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
onPick: (placeholder) {
|
child: Row(
|
||||||
// Insert placeholder at current cursor position
|
children: [
|
||||||
final text = messageController.text;
|
const Icon(
|
||||||
final selection = messageController.selection;
|
Symbols.more_horiz,
|
||||||
final start =
|
size: 16,
|
||||||
selection.start >= 0
|
).padding(horizontal: 8),
|
||||||
? selection.start
|
const Gap(8),
|
||||||
: text.length;
|
Expanded(
|
||||||
final end =
|
child: Text(
|
||||||
selection.end >= 0
|
'typingHint'.plural(
|
||||||
? selection.end
|
chatSubscribe.length,
|
||||||
: text.length;
|
args: [
|
||||||
final newText = text.replaceRange(
|
chatSubscribe
|
||||||
start,
|
.map((x) => x.nick ?? x.account.nick)
|
||||||
end,
|
.join(', '),
|
||||||
placeholder,
|
],
|
||||||
);
|
),
|
||||||
messageController.value = TextEditingValue(
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
text: newText,
|
),
|
||||||
selection: TextSelection.collapsed(
|
|
||||||
offset: start + placeholder.length,
|
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: 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);
|
||||||
},
|
},
|
||||||
);
|
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
PopupMenuButton(
|
},
|
||||||
icon: const Icon(Symbols.photo_library),
|
separatorBuilder: (_, _) => const Gap(8),
|
||||||
itemBuilder:
|
),
|
||||||
(context) => [
|
).padding(vertical: 12),
|
||||||
PopupMenuItem(
|
if (messageReplyingTo != null ||
|
||||||
onTap: () => onPickFile(true),
|
messageForwardingTo != null ||
|
||||||
child: Row(
|
messageEditingTo != null)
|
||||||
spacing: 12,
|
Container(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(
|
||||||
const Icon(Symbols.photo),
|
horizontal: 16,
|
||||||
Text('addPhoto').tr(),
|
vertical: 4,
|
||||||
],
|
),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
PopupMenuItem(
|
borderRadius: BorderRadius.circular(32),
|
||||||
onTap: () => onPickFile(false),
|
),
|
||||||
child: Row(
|
margin: const EdgeInsets.only(
|
||||||
spacing: 12,
|
left: 8,
|
||||||
children: [
|
right: 8,
|
||||||
const Icon(Symbols.video_call),
|
top: 8,
|
||||||
Text('addVideo').tr(),
|
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(
|
Row(
|
||||||
child: RawKeyboardListener(
|
children: [
|
||||||
focusNode: FocusNode(),
|
Row(
|
||||||
onKey: (event) => handleKeyPress(context, ref, event),
|
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(
|
child: TextField(
|
||||||
focusNode: inputFocusNode,
|
focusNode: inputFocusNode,
|
||||||
controller: messageController,
|
controller: messageController,
|
||||||
onSubmitted:
|
keyboardType: TextInputType.multiline,
|
||||||
(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;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText:
|
||||||
(chatRoom.type == 1 && chatRoom.name == null)
|
(chatRoom.type == 1 && chatRoom.name == null)
|
||||||
@@ -314,16 +380,16 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
icon: const Icon(Icons.send),
|
||||||
icon: const Icon(Icons.send),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
onPressed: send,
|
||||||
onPressed: send,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:island/models/chat.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:island/widgets/content/markdown.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:pretty_diff_text/pretty_diff_text.dart';
|
import 'package:pretty_diff_text/pretty_diff_text.dart';
|
||||||
@@ -56,7 +56,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
case 'messages.update.links':
|
case 'messages.update.links':
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.edit,
|
Symbols.edit,
|
||||||
@@ -64,27 +64,29 @@ class MessageContent extends StatelessWidget {
|
|||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||||
),
|
).padding(top: 2),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
if (item.meta['previous_content'] is String)
|
if (item.meta['previous_content'] is String)
|
||||||
PrettyDiffText(
|
Flexible(
|
||||||
oldText: item.meta['previous_content'],
|
child: PrettyDiffText(
|
||||||
newText: item.content ?? 'Edited a message',
|
oldText: item.meta['previous_content'],
|
||||||
defaultTextStyle: Theme.of(
|
newText: item.content ?? 'Edited a message',
|
||||||
context,
|
defaultTextStyle: Theme.of(
|
||||||
).textTheme.bodyMedium!.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
addedTextStyle: TextStyle(
|
|
||||||
backgroundColor: Theme.of(
|
|
||||||
context,
|
context,
|
||||||
).colorScheme.primaryFixedDim.withOpacity(0.4),
|
).textTheme.bodyMedium!.copyWith(
|
||||||
),
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
deletedTextStyle: TextStyle(
|
),
|
||||||
decoration: TextDecoration.lineThrough,
|
addedTextStyle: TextStyle(
|
||||||
color: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withOpacity(0.7),
|
).colorScheme.primaryFixedDim.withOpacity(0.4),
|
||||||
|
),
|
||||||
|
deletedTextStyle: TextStyle(
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant.withOpacity(0.7),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -104,10 +106,12 @@ class MessageContent extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MarkdownTextContent(
|
Flexible(
|
||||||
content: item.content ?? '*${item.type} has no content*',
|
child: MarkdownTextContent(
|
||||||
isSelectable: true,
|
content: item.content ?? '*${item.type} has no content*',
|
||||||
linesMargin: EdgeInsets.zero,
|
isSelectable: true,
|
||||||
|
linesMargin: EdgeInsets.zero,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (translatedText?.isNotEmpty ?? false)
|
if (translatedText?.isNotEmpty ?? false)
|
||||||
...([
|
...([
|
||||||
|
@@ -11,10 +11,10 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
import 'package:island/models/embed.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/translate.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/screens/chat/room.dart';
|
|
||||||
import 'package:island/utils/mapping.dart';
|
import 'package:island/utils/mapping.dart';
|
||||||
import 'package:island/widgets/account/account_pfc.dart';
|
import 'package:island/widgets/account/account_pfc.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
@@ -538,7 +538,7 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
|
|||||||
isMultiline ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
isMultiline ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
DateFormat('HH:mm').format(message.createdAt),
|
DateFormat('HH:mm').format(message.createdAt.toLocal()),
|
||||||
style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12),
|
style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12),
|
||||||
).padding(top: isMultiline ? 2 : 0),
|
).padding(top: isMultiline ? 2 : 0),
|
||||||
AccountPfcGestureDetector(
|
AccountPfcGestureDetector(
|
||||||
|
@@ -6,7 +6,7 @@ import "package:gap/gap.dart";
|
|||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:island/database/message.dart";
|
import "package:island/database/message.dart";
|
||||||
import "package:island/models/chat.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/pods/network.dart";
|
||||||
import "package:island/services/responsive.dart";
|
import "package:island/services/responsive.dart";
|
||||||
import "package:island/widgets/alert.dart";
|
import "package:island/widgets/alert.dart";
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/message.dart';
|
import 'package:island/pods/message.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/websocket.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/network_status_sheet.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -65,69 +68,97 @@ class DebugSheet extends HookConsumerWidget {
|
|||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: 'Debug',
|
titleText: 'Debug',
|
||||||
heightFactor: 0.6,
|
heightFactor: 0.6,
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
children: [
|
child: Column(
|
||||||
ListTile(
|
children: [
|
||||||
minTileHeight: 48,
|
const Gap(4),
|
||||||
leading: const Icon(Symbols.wifi),
|
ListTile(
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
minTileHeight: 48,
|
||||||
title: Text('Connection Status'),
|
leading: const Icon(Symbols.update),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
title: Text('Force Update'),
|
||||||
showModalBottomSheet(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
context: context,
|
onTap: () async {
|
||||||
isScrollControlled: true,
|
// Fetch latest release and show the unified sheet
|
||||||
builder:
|
final svc = UpdateService();
|
||||||
(context) => NetworkStatusSheet(
|
// Reuse service fetch + compare to decide content
|
||||||
onReconnect: () => wsNotifier.connect(),
|
showLoadingModal(context);
|
||||||
),
|
final release = await svc.fetchLatestRelease();
|
||||||
);
|
if (!context.mounted) return;
|
||||||
},
|
hideLoadingModal(context);
|
||||||
),
|
if (release != null) {
|
||||||
const Divider(height: 1),
|
await svc.showUpdateSheet(context, release);
|
||||||
ListTile(
|
} else {
|
||||||
minTileHeight: 48,
|
showInfoAlert(
|
||||||
leading: const Icon(Symbols.copy_all),
|
'Currently cannot get update from the GitHub.',
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
'Unable to check for updates',
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
);
|
||||||
title: Text('Copy access token'),
|
}
|
||||||
onTap: () async {
|
}
|
||||||
final tk = ref.watch(tokenProvider);
|
),
|
||||||
Clipboard.setData(ClipboardData(text: tk!.token));
|
const Divider(height: 8),
|
||||||
},
|
ListTile(
|
||||||
),
|
minTileHeight: 48,
|
||||||
ListTile(
|
leading: const Icon(Symbols.wifi),
|
||||||
minTileHeight: 48,
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
leading: const Icon(Symbols.edit),
|
title: Text('Connection Status'),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
onTap: () {
|
||||||
title: Text('Set access token'),
|
showModalBottomSheet(
|
||||||
onTap: () async {
|
context: context,
|
||||||
await _showSetTokenDialog(context, ref);
|
isScrollControlled: true,
|
||||||
},
|
builder:
|
||||||
),
|
(context) => NetworkStatusSheet(
|
||||||
const Divider(height: 1),
|
onReconnect: () => wsNotifier.connect(),
|
||||||
ListTile(
|
),
|
||||||
minTileHeight: 48,
|
);
|
||||||
leading: const Icon(Symbols.delete),
|
},
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
const Divider(height: 8),
|
||||||
title: Text('Reset database'),
|
ListTile(
|
||||||
onTap: () async {
|
minTileHeight: 48,
|
||||||
resetDatabase(ref);
|
leading: const Icon(Symbols.copy_all),
|
||||||
},
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
),
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
ListTile(
|
title: Text('Copy access token'),
|
||||||
minTileHeight: 48,
|
onTap: () async {
|
||||||
leading: const Icon(Symbols.clear),
|
final tk = ref.watch(tokenProvider);
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
Clipboard.setData(ClipboardData(text: tk!.token));
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
},
|
||||||
title: Text('Clear cache'),
|
),
|
||||||
onTap: () async {
|
ListTile(
|
||||||
DefaultCacheManager().emptyCache();
|
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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -80,9 +81,8 @@ class PostItemCreator extends HookConsumerWidget {
|
|||||||
title: 'copyLink'.tr(),
|
title: 'copyLink'.tr(),
|
||||||
image: MenuImage.icon(Symbols.link),
|
image: MenuImage.icon(Symbols.link),
|
||||||
callback: () {
|
callback: () {
|
||||||
context.pushNamed(
|
Clipboard.setData(
|
||||||
'postDetail',
|
ClipboardData(text: 'https://solian.app/posts/${item.id}'),
|
||||||
pathParameters: {'id': item.id},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -95,7 +95,7 @@ class PostItemCreator extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isOpenable) {
|
if (isOpenable) {
|
||||||
context.goNamed('postDetail', pathParameters: {'id': item.id});
|
context.pushNamed('postDetail', pathParameters: {'id': item.id});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
|
||||||
#include <file_saver/file_saver_plugin.h>
|
#include <file_saver/file_saver_plugin.h>
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_platform_alert/flutter_platform_alert_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 <media_kit_video/media_kit_video_plugin.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
#include <record_linux/record_linux_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 <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
#include <super_native_extensions/super_native_extensions_plugin.h>
|
#include <super_native_extensions/super_native_extensions_plugin.h>
|
||||||
#include <tray_manager/tray_manager_plugin.h>
|
#include <tray_manager/tray_manager_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
#include <volume_controller/volume_controller_plugin.h>
|
#include <volume_controller/volume_controller_plugin.h>
|
||||||
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
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 =
|
g_autoptr(FlPluginRegistrar) file_saver_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
||||||
file_saver_plugin_register_with_registrar(file_saver_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) record_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
|
||||||
record_linux_plugin_register_with_registrar(record_linux_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||||
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
|
||||||
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
|
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
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
bitsdojo_window_linux
|
|
||||||
file_saver
|
file_saver
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_platform_alert
|
flutter_platform_alert
|
||||||
@@ -17,11 +16,13 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
media_kit_video
|
media_kit_video
|
||||||
pasteboard
|
pasteboard
|
||||||
record_linux
|
record_linux
|
||||||
|
screen_retriever_linux
|
||||||
sqlite3_flutter_libs
|
sqlite3_flutter_libs
|
||||||
super_native_extensions
|
super_native_extensions
|
||||||
tray_manager
|
tray_manager
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
volume_controller
|
volume_controller
|
||||||
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
@@ -6,19 +6,20 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
|
||||||
|
|
||||||
struct _MyApplication {
|
struct _MyApplication
|
||||||
|
{
|
||||||
GtkApplication parent_instance;
|
GtkApplication parent_instance;
|
||||||
char** dart_entrypoint_arguments;
|
char **dart_entrypoint_arguments;
|
||||||
};
|
};
|
||||||
|
|
||||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||||
|
|
||||||
// Implements GApplication::activate.
|
// Implements GApplication::activate.
|
||||||
static void my_application_activate(GApplication* application) {
|
static void my_application_activate(GApplication *application)
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
{
|
||||||
GtkWindow* window =
|
MyApplication *self = MY_APPLICATION(application);
|
||||||
|
GtkWindow *window =
|
||||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
|
||||||
// Use a header bar when running in GNOME as this is the common style used
|
// 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).
|
// if future cases occur).
|
||||||
gboolean use_header_bar = TRUE;
|
gboolean use_header_bar = TRUE;
|
||||||
#ifdef GDK_WINDOWING_X11
|
#ifdef GDK_WINDOWING_X11
|
||||||
GdkScreen* screen = gtk_window_get_screen(window);
|
GdkScreen *screen = gtk_window_get_screen(window);
|
||||||
if (GDK_IS_X11_SCREEN(screen)) {
|
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) {
|
const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||||
|
if (g_strcmp0(wm_name, "GNOME Shell") != 0)
|
||||||
|
{
|
||||||
use_header_bar = FALSE;
|
use_header_bar = FALSE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (use_header_bar) {
|
if (use_header_bar)
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
{
|
||||||
|
GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
gtk_header_bar_set_title(header_bar, "island");
|
gtk_header_bar_set_title(header_bar, "island");
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
gtk_window_set_title(window, "island");
|
gtk_window_set_title(window, "island");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto bdw = bitsdojo_window_from(window);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
bdw->setCustomFrame(true);
|
|
||||||
gtk_widget_show(GTK_WIDGET(window));
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
|
||||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
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_widget_show(GTK_WIDGET(view));
|
||||||
gtk_container_add(GTK_CONTAINER(window), 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.
|
// Implements GApplication::local_command_line.
|
||||||
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
{
|
||||||
|
MyApplication *self = MY_APPLICATION(application);
|
||||||
// Strip out the first argument as it is the binary name.
|
// Strip out the first argument as it is the binary name.
|
||||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||||
|
|
||||||
g_autoptr(GError) error = nullptr;
|
g_autoptr(GError) error = nullptr;
|
||||||
if (!g_application_register(application, nullptr, &error)) {
|
if (!g_application_register(application, nullptr, &error))
|
||||||
g_warning("Failed to register: %s", error->message);
|
{
|
||||||
*exit_status = 1;
|
g_warning("Failed to register: %s", error->message);
|
||||||
return TRUE;
|
*exit_status = 1;
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_application_activate(application);
|
g_application_activate(application);
|
||||||
@@ -84,8 +91,9 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implements GApplication::startup.
|
// Implements GApplication::startup.
|
||||||
static void my_application_startup(GApplication* application) {
|
static void my_application_startup(GApplication *application)
|
||||||
//MyApplication* self = MY_APPLICATION(object);
|
{
|
||||||
|
// MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
// Perform any actions required at application startup.
|
// Perform any actions required at application startup.
|
||||||
|
|
||||||
@@ -93,8 +101,9 @@ static void my_application_startup(GApplication* application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implements GApplication::shutdown.
|
// Implements GApplication::shutdown.
|
||||||
static void my_application_shutdown(GApplication* application) {
|
static void my_application_shutdown(GApplication *application)
|
||||||
//MyApplication* self = MY_APPLICATION(object);
|
{
|
||||||
|
// MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
// Perform any actions required at application shutdown.
|
// Perform any actions required at application shutdown.
|
||||||
|
|
||||||
@@ -102,13 +111,15 @@ static void my_application_shutdown(GApplication* application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implements GObject::dispose.
|
// Implements GObject::dispose.
|
||||||
static void my_application_dispose(GObject* object) {
|
static void my_application_dispose(GObject *object)
|
||||||
MyApplication* self = MY_APPLICATION(object);
|
{
|
||||||
|
MyApplication *self = MY_APPLICATION(object);
|
||||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
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)->activate = my_application_activate;
|
||||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
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;
|
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
|
// Set the program name to the application ID, which helps various systems
|
||||||
// like GTK and desktop environments map this running application to its
|
// like GTK and desktop environments map this running application to its
|
||||||
// corresponding .desktop file. This ensures better integration by allowing
|
// corresponding .desktop file. This ensures better integration by allowing
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import bitsdojo_window_macos
|
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
import file_picker
|
import file_picker
|
||||||
@@ -32,6 +31,7 @@ import package_info_plus
|
|||||||
import pasteboard
|
import pasteboard
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import record_macos
|
import record_macos
|
||||||
|
import screen_retriever_macos
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sign_in_with_apple
|
import sign_in_with_apple
|
||||||
@@ -42,9 +42,9 @@ import tray_manager
|
|||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import volume_controller
|
import volume_controller
|
||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
|
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
@@ -71,6 +71,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||||
|
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
||||||
@@ -81,4 +82,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
||||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- bitsdojo_window_macos (0.0.1):
|
|
||||||
- FlutterMacOS
|
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
@@ -202,6 +200,8 @@ PODS:
|
|||||||
- record_macos (1.1.0):
|
- record_macos (1.1.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
|
- screen_retriever_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -248,9 +248,10 @@ PODS:
|
|||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (137.7151.04)
|
- WebRTC-SDK (137.7151.04)
|
||||||
|
- window_manager (0.5.0):
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
|
|
||||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/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`)
|
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
- 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`)
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- 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`)
|
- 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`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
|
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
|
||||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||||
|
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -314,8 +317,6 @@ SPEC REPOS:
|
|||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
bitsdojo_window_macos:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
|
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||||
croppy:
|
croppy:
|
||||||
@@ -372,6 +373,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||||
record_macos:
|
record_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||||
|
screen_retriever_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
|
||||||
share_plus:
|
share_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
@@ -392,9 +395,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||||
|
window_manager:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
|
|
||||||
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||||
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
||||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||||
@@ -441,6 +445,7 @@ SPEC CHECKSUMS:
|
|||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
|
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
|
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
|
||||||
@@ -453,6 +458,7 @@ SPEC CHECKSUMS:
|
|||||||
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
||||||
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||||
|
|
||||||
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f
|
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f
|
||||||
|
|
||||||
|
@@ -4,10 +4,25 @@ import FlutterMacOS
|
|||||||
@main
|
@main
|
||||||
class AppDelegate: FlutterAppDelegate {
|
class AppDelegate: FlutterAppDelegate {
|
||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
return true
|
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 Cocoa
|
||||||
import FlutterMacOS
|
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() {
|
override func awakeFromNib() {
|
||||||
let flutterViewController = FlutterViewController()
|
let flutterViewController = FlutterViewController()
|
||||||
let windowFrame = self.frame
|
let windowFrame = self.frame
|
||||||
@@ -17,4 +12,4 @@ class MainFlutterWindow: BitsdojoWindow {
|
|||||||
|
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
}
|
}
|
||||||
}
|
}
|
92
pubspec.lock
92
pubspec.lock
@@ -81,46 +81,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1201,10 +1161,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: google_fonts
|
name: google_fonts
|
||||||
sha256: ebc94ed30fd13cefd397cb1658b593f21571f014b7d1197eeb41fb95f05d899a
|
sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.1"
|
version: "6.3.2"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2125,6 +2085,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
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:
|
screenshot:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2796,6 +2796,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
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:
|
windows_notification:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@@ -38,7 +38,6 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_hooks: ^0.21.3+1
|
flutter_hooks: ^0.21.3+1
|
||||||
hooks_riverpod: ^2.6.1
|
hooks_riverpod: ^2.6.1
|
||||||
bitsdojo_window: ^0.1.6
|
|
||||||
go_router: ^16.2.4
|
go_router: ^16.2.4
|
||||||
styled_widget: ^0.4.1
|
styled_widget: ^0.4.1
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
@@ -53,7 +52,7 @@ dependencies:
|
|||||||
flutter_highlight: ^0.7.0
|
flutter_highlight: ^0.7.0
|
||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
google_fonts: ^6.3.1
|
google_fonts: ^6.3.2
|
||||||
gap: ^3.0.1
|
gap: ^3.0.1
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
web: ^1.1.1
|
web: ^1.1.1
|
||||||
@@ -153,6 +152,7 @@ dependencies:
|
|||||||
ffi: ^2.1.4
|
ffi: ^2.1.4
|
||||||
dart_ipc: ^1.0.1
|
dart_ipc: ^1.0.1
|
||||||
pretty_diff_text: ^2.1.0
|
pretty_diff_text: ^2.1.0
|
||||||
|
window_manager: ^0.5.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
||||||
#include <file_saver/file_saver_plugin.h>
|
#include <file_saver/file_saver_plugin.h>
|
||||||
@@ -26,17 +25,17 @@
|
|||||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
#include <record_windows/record_windows_plugin_c_api.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 <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
||||||
#include <tray_manager/tray_manager_plugin.h>
|
#include <tray_manager/tray_manager_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <volume_controller/volume_controller_plugin_c_api.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>
|
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
|
||||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
DartIpcPluginCApiRegisterWithRegistrar(
|
DartIpcPluginCApiRegisterWithRegistrar(
|
||||||
@@ -75,6 +74,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||||
|
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||||
@@ -87,6 +88,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
VolumeControllerPluginCApiRegisterWithRegistrar(
|
VolumeControllerPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
|
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
|
||||||
|
WindowManagerPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||||
WindowsNotificationPluginCApiRegisterWithRegistrar(
|
WindowsNotificationPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
|
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
bitsdojo_window_windows
|
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
dart_ipc
|
dart_ipc
|
||||||
file_saver
|
file_saver
|
||||||
@@ -23,12 +22,14 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
media_kit_video
|
media_kit_video
|
||||||
pasteboard
|
pasteboard
|
||||||
record_windows
|
record_windows
|
||||||
|
screen_retriever_windows
|
||||||
share_plus
|
share_plus
|
||||||
sqlite3_flutter_libs
|
sqlite3_flutter_libs
|
||||||
super_native_extensions
|
super_native_extensions
|
||||||
tray_manager
|
tray_manager
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
volume_controller
|
volume_controller
|
||||||
|
window_manager
|
||||||
windows_notification
|
windows_notification
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -5,14 +5,21 @@
|
|||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
#include "utils.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,
|
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
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
// new console when running with a debugger.
|
// new console when running with a debugger.
|
||||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())
|
||||||
|
{
|
||||||
CreateAndAttachConsole();
|
CreateAndAttachConsole();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,13 +37,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(1280, 720);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.Create(L"Solian", origin, size)) {
|
if (!window.Create(L"Solian", origin, size))
|
||||||
|
{
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|
||||||
::MSG msg;
|
::MSG msg;
|
||||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
while (::GetMessage(&msg, nullptr, 0, 0))
|
||||||
|
{
|
||||||
::TranslateMessage(&msg);
|
::TranslateMessage(&msg);
|
||||||
::DispatchMessage(&msg);
|
::DispatchMessage(&msg);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user