Compare commits

..

51 Commits

Author SHA1 Message Date
3fd9cd4547 Leveling screen basis 2025-06-22 03:37:29 +08:00
beda47ff7e 👽 Update afdian connection 2025-06-22 03:16:09 +08:00
d1506f10ef 💄 Optimized call 2025-06-22 01:40:10 +08:00
006841cf82 Pin code
🐛 Bug fixes
2025-06-22 00:38:51 +08:00
f73cf10a54 Blurry tab background
♻️ Refactored tabs
2025-06-21 21:11:46 +08:00
b39248cc58 🐛 Fix nullable link preview meta fields 2025-06-21 16:52:10 +08:00
4ba809a8d6 Renders embedded links 2025-06-21 15:19:32 +08:00
a6d1ca57d7 Post truncate hint 2025-06-21 12:11:17 +08:00
fbbe373ce8 💄 Optimized compose page 2025-06-21 11:10:42 +08:00
91c5a2e1b6 🚀 Launch 3.0.0+106 2025-06-20 01:35:45 +08:00
cb991d1574 🐛 Fixes notification wrong bubble count 2025-06-20 01:33:32 +08:00
75097ab6fc 🐛 Fix the user agent 2025-06-20 01:31:23 +08:00
8d855867c1 Replies sheet is back
🗑️ The silver paging helper is merged, remove the one inside the own codebase
2025-06-20 01:11:17 +08:00
89fd80bcb8 Article editor 2025-06-19 23:38:38 +08:00
ab4f4faafe 🎨 Optimized code of post compose 2025-06-19 00:13:36 +08:00
52111c4b95 Creator's post list 2025-06-18 23:44:47 +08:00
15a5848785 Client side remove GPS EXIF 2025-06-18 23:07:51 +08:00
e29a2fc054 💄 Optimized oidc login 2025-06-18 13:15:13 +08:00
7f4e489f51 👽 Update the OIDC login 2025-06-18 01:45:53 +08:00
eb4d2c2e2f Finish up connections 2025-06-17 23:49:46 +08:00
9b67d58ee4 🐛 Fixes some bugs in OIDC 2025-06-17 00:18:41 +08:00
4dbee27718 🧱 OpenID Connect client infra 2025-06-16 01:44:24 +08:00
4b9c9aec92 🐛 Fixes ignored wrong exception in sign in with apple 2025-06-16 01:30:19 +08:00
00b3dc7be6 Connection management 2025-06-16 01:25:03 +08:00
7f26196e85 🎨 Split up account settings page 2025-06-16 00:53:26 +08:00
3e5669780f 🐛 Fixes bugs on OAuth 2025-06-15 23:38:24 +08:00
484ded03b1 🧱 Add login with apple to web 2025-06-15 17:43:22 +08:00
b3786827ef Login with apple 2025-06-15 17:29:41 +08:00
217a0c0a54 🐛 Fix notification push doesn't work 2025-06-15 01:09:24 +08:00
5c0f7225e6 🐛 Fixes 2025-06-15 00:59:11 +08:00
a7cb7170b8 🐛 Fix call unnesssary rejoining 2025-06-15 00:18:10 +08:00
2fac5e5383 💄 Optimized notification toast 2025-06-15 00:17:59 +08:00
00b9c4b957 In-app notification toast basis 2025-06-14 16:54:29 +08:00
6fbf3d9fc4 Support links in notification 2025-06-14 16:54:21 +08:00
36fb06b81c Post date 2025-06-14 15:39:39 +08:00
129c215a02 Finishing up the profile page data displaying 2025-06-14 11:39:09 +08:00
0f125f45f0 Better nameplate 2025-06-14 02:29:21 +08:00
30416f7ca0 💄 Optimized publisher profile & personal profile 2025-06-14 02:16:52 +08:00
6e74cf3a93 Profile card 2025-06-13 01:06:37 +08:00
0d6424e155 Fortune graph 2025-06-12 23:56:01 +08:00
0424eb0c2a Enrich user profile settings 2025-06-12 22:21:58 +08:00
be01bac5db ⬆️ Upgrade flutter and remove override deps which their issue closed
🐛 Fix pfp widget cause layout error
2025-06-11 22:20:19 +08:00
7bb4e68054 Able to generate color scheme from background image 2025-06-11 00:56:20 +08:00
61e61866d7 Dynamic generate color to fit user background image 2025-06-11 00:47:26 +08:00
280c261ea1 🐛 Fix profile picture has no content 2025-06-11 00:24:53 +08:00
49bc6b9a98 Login now not use the password as default factor 2025-06-11 00:24:45 +08:00
461f32545a 💄 Optimized profile page background image 2025-06-11 00:20:21 +08:00
36b9026e9e Verification mark 2025-06-11 00:09:19 +08:00
78f258dcea 🎨 Fix some warnings in the code 2025-06-10 23:29:32 +08:00
044fb983d6 Chat break and notify level 2025-06-10 01:09:28 +08:00
2a7876e22f 🐛 Optimized typing indicator 2025-06-09 23:50:19 +08:00
126 changed files with 11447 additions and 3128 deletions

View File

@ -18,9 +18,7 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
defaultConfig {
applicationId = "dev.solsynth.solian"
@ -32,11 +30,20 @@ android {
versionName = flutter.versionName
}
signingConfigs {
release {
keyAlias = keystoreProperties['keyAlias']
keyPassword = keystoreProperties['keyPassword']
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword = keystoreProperties['storePassword']
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = signingConfigs.getByName("release")
minifyEnabled = true
shrinkResources = true
}
}
}

View File

@ -42,6 +42,22 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Sign in with Apple -->
<activity
android:name="com.aboutyou.dart_packages.sign_in_with_apple.SignInWithAppleCallback"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="signinwithapple" />
<data android:path="callback" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="dev.solsynth.solian.provider"

View File

@ -10,6 +10,8 @@
"loginEnterPassword": "Enter the code",
"loginSuccess": "Logged in as {}",
"loginGreeting": "Welcome back!",
"loginOr": "Or login with\nthird parties",
"loginInProgress": "Logging you in...",
"username": "Username",
"usernameCannotChangeHint": "Username cannot be updated after created.",
"usernameLookupHint": "We also take your email address.",
@ -27,7 +29,7 @@
"fieldCannotBeEmpty": "This field cannot be empty.",
"fieldEmailAddressMustBeValid": "The email address must be valid.",
"logout": "Logout",
"updateYourProfile": "Edit Profile",
"updateYourProfile": "Profile Settings",
"accountBasicInfo": "Basic Info",
"accountProfile": "Your Profile",
"saveChanges": "Save Changes",
@ -63,6 +65,8 @@
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
"authFactorInAppNotify": "In-app notification",
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorPin": "Pin Code",
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
"realms": "Realms",
"createRealm": "Create a Realm",
"createRealmHint": "Meet friends with same interests, build communities, and more.",
@ -70,6 +74,8 @@
"deleteRealm": "Delete Realm",
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
"explore": "Explore",
"exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends",
"account": "Account",
"name": "Name",
"description": "Description",
@ -98,6 +104,11 @@
"permissionModerator": "Moderator",
"permissionMember": "Member",
"reply": "Reply",
"repliesCount": {
"zero": "No reply",
"one": "{} reply",
"other": "{} replies"
},
"forward": "Forward",
"repliedTo": "Replied to",
"forwarded": "Forwarded",
@ -113,7 +124,8 @@
"addVideo": "Add video",
"addPhoto": "Add photo",
"addFile": "Add file",
"createDirectMessage": "New direct message",
"createDirectMessage": "Send new DM",
"gotoDirectMessage": "Go to DM",
"react": "React",
"reactions": {
"zero": "Reactions",
@ -126,6 +138,25 @@
"connectionConnected": "Connected",
"connectionDisconnected": "Disconnected",
"connectionReconnecting": "Reconnecting",
"accountConnections": "Account Connections",
"accountConnectionsDescription": "Manage your external account connections",
"accountConnectionAdd": "Add Connection",
"accountConnectionDelete": "Delete Connection",
"accountConnectionDeleteHint": "Are you sure you want to delete this connection? This action cannot be undone.",
"accountConnectionsEmpty": "No connections found. Add a connection to get started.",
"accountConnectionProvider": "Provider",
"accountConnectionProviderHint": "Enter provider name",
"accountConnectionIdentifier": "Identifier",
"accountConnectionIdentifierHint": "Enter your identifier for this provider",
"accountConnectionDescription": "Add a connection to link your account with external services.",
"accountConnectionAddSuccess": "Connection added successfully.",
"accountConnectionAddError": "Unable to setup connection.",
"accountConnectionProviderApple": "Apple",
"accountConnectionProviderMicrosoft": "Microsoft",
"accountConnectionProviderGoogle": "Google",
"accountConnectionProviderGithub": "GitHub",
"accountConnectionProviderDiscord": "Discord",
"accountConnectionProviderAfdian": "Afdian",
"checkIn": "Check In",
"checkInNone": "Not checked-in yet",
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
@ -134,14 +165,11 @@
"checkInResultLevel2": "A Normal Day",
"checkInResultLevel3": "Good Luck",
"checkInResultLevel4": "Best Luck",
"checkInResultLevelShort0": "Wrost",
"checkInResultLevelShort1": "Bad",
"checkInResultLevelShort2": "Normal",
"checkInResultLevelShort3": "Good",
"checkInResultLevelShort4": "Best",
"checkInActivityTitle": "{} checked in on {} and got a {}",
"eventCalander": "Event Calander",
"eventCalanderEmpty": "No events on that day.",
"fortuneGraph": "Fortune Trend",
"noFortuneData": "No fortune data available for this month.",
"creatorHub": "Creator Hub",
"creatorHubDescription": "Manage posts, analytics, and more.",
"developerPortal": "Developer Portal",
@ -233,6 +261,7 @@
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
"relationships": "Relationships",
"addFriend": "Send a Friend Request",
"addFriendShort": "Add as Friend",
"addFriendHint": "Add a friend to your relationship list.",
"pendingRequest": "Pending",
"waitingRequest": "Waiting",
@ -277,11 +306,13 @@
"posts": "Posts",
"settingsBackgroundImage": "Background Image",
"settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
},
"chatBreakNone": "None",
"settingsRealmCompactView": "Compact Realm View",
"settingsMixedFeed": "Mixed Feed",
"settingsAutoTranslate": "Auto Translate",
@ -315,12 +346,16 @@
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
"unauthorized": "Unauthorized",
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
"publisherVisitAccountPage": "Visit the profile of {}",
"publisherBelongsTo": "Belongs to {}",
"postContent": "Content",
"postSettings": "Settings",
"postPublisherUnselected": "Publisher Unspecified",
"postVisibility": "Visibility",
"postVisibilityPublic": "Public",
"postVisibilityFriends": "Friends Only",
"postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private",
"postTruncated": "Content truncated, tap to view full post",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor",
@ -396,5 +431,41 @@
"contactMethodPrimary": "Primary",
"contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone."
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"chatNotifyLevel": "Notify Level",
"chatNotifyLevelDescription": "Decide how many notifications you will receive.",
"chatNotifyLevelAll": "All",
"chatNotifyLevelMention": "Mentions",
"chatNotifyLevelNone": "None",
"chatNotifyLevelUpdated": "The notify level has been updated to {}.",
"chatBreak": "Take a Break",
"chatBreakDescription": "Set a time, before that time, your notification level will be metions only, to take a break of the current topic they're talking about.",
"chatBreakClear": "Clear the break time",
"chatBreakHour": "{} break",
"chatBreakDay": "{} day break",
"chatBreakSet": "Break set for {}",
"chatBreakCleared": "Chat break has been cleared.",
"chatBreakCustom": "Custom duration",
"chatBreakEnterMinutes": "Enter minutes",
"firstName": "First Name",
"middleName": "Middle Name",
"lastName": "Last Name",
"gender": "Gender",
"pronouns": "Pronouns",
"location": "Location",
"timeZone": "Time Zone",
"birthday": "Birthday",
"selectADate": "Select a date",
"checkInResultT0": "Worst",
"checkInResultT1": "Poor",
"checkInResultT2": "Mid",
"checkInResultT3": "Good",
"checkInResultT4": "Best",
"accountProfileView": "View Profile",
"unspecified": "Unspecified",
"added": "Added",
"preview": "Preview",
"togglePreview": "Toggle Preview",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe"
}

View File

@ -291,5 +291,27 @@
"postVisibilityPublic": "公开",
"postVisibilityFriends": "仅好友可见",
"postVisibilityUnlisted": "不公开",
"postVisibilityPrivate": "私密"
"postVisibilityPrivate": "私密",
"postTruncated": "内容已截断,点击查看完整帖子",
"chatNotifyLevel": "通知级别",
"chatNotifyLevelDescription": "决定您将收到多少通知。",
"chatNotifyLevelAll": "全部",
"chatNotifyLevelMention": "提及",
"chatNotifyLevelNone": "无",
"chatNotifyLevelUpdated": "通知级别已更新为 {}。",
"chatBreak": "暂停聊天",
"chatBreakDescription": "设置一个时间,在该时间之前,您的通知级别将仅为提及,以暂时休息当前讨论的话题。",
"chatBreakClear": "清除暂停时间",
"chatBreakHour": "暂停 {} 分钟",
"chatBreakDay": "暂停 {} 天",
"chatBreakSet": "已设置暂停 {}",
"chatBreakCleared": "聊天暂停已清除。",
"chatBreakCustom": "自定义时长",
"chatBreakEnterMinutes": "输入分钟数",
"chatBreakNone": "无",
"checkInResultT0": "大凶",
"checkInResultT1": "凶",
"checkInResultT2": "中平",
"checkInResultT3": "吉",
"checkInResultT4": "大吉"
}

View File

@ -68,7 +68,7 @@
"createRealmHint": "結識志同道合的朋友、建立社群等等。",
"editRealm": "編輯領域",
"deleteRealm": "刪除領域",
"deleteRealmHint": "確定要刪除此領域嗎?這也將刪除領域下的所有頻道、發佈者和貼文。",
"deleteRealmHint": "確定要刪除此領域嗎?這也將刪除領域下的所有頻道、發佈者和貼文。",
"explore": "探索",
"account": "帳號",
"name": "名稱",
@ -291,5 +291,21 @@
"postVisibilityPublic": "公開",
"postVisibilityFriends": "僅好友可見",
"postVisibilityUnlisted": "不公開",
"postVisibilityPrivate": "私密"
"postVisibilityPrivate": "私密",
"chatNotifyLevel": "通知等級",
"chatNotifyLevelDescription": "決定您將收到多少通知。",
"chatNotifyLevelAll": "全部",
"chatNotifyLevelMention": "提及",
"chatNotifyLevelNone": "無",
"chatNotifyLevelUpdated": "通知等級已更新為 {}。",
"chatBreak": "暫停聊天",
"chatBreakDescription": "設定一個時間,在該時間之前,您的通知等級將僅為提及,以暫時休息當前討論的話題。",
"chatBreakClear": "清除暫停時間",
"chatBreakHour": "暫停 {} 分鐘",
"chatBreakDay": "暫停 {} 天",
"chatBreakSet": "已設定暫停 {}",
"chatBreakCleared": "聊天暫停已清除。",
"chatBreakCustom": "自訂時長",
"chatBreakEnterMinutes": "輸入分鐘數",
"chatBreakNone": "無"
}

View File

@ -0,0 +1,10 @@
<svg
viewBox="0 0 160 160"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M134.614 98.3714C133.294 97.5334 131.909 97.1697 130.563 97.02C133.724 89.3002 135.736 79.1949 128.887 69.1574C118.406 53.7998 103.38 45.8198 84.2346 45.4382C78.7809 45.3312 72.3517 45.5844 65.5487 45.8554C57.6493 46.1692 47.1369 46.5793 39.9921 45.9873C41.4161 45.2136 42.9326 44.4719 44.2462 43.8336C49.2728 41.384 53.2314 39.4763 51.9214 36.0925C51.2343 34.117 49.1874 33.0794 45.8233 33.0045C38.7426 32.8441 23.4421 36.9447 20.6903 43.8586C19.1418 47.7524 18.8854 55.2689 34.5668 61.9119C41.0174 64.6503 59.237 67.9879 66.2678 68.6867C68.2542 68.8793 69.7743 69.2822 70.9277 69.7101C69.3151 70.7727 67.6597 71.8888 65.9972 73.0298C63.1102 71.3824 58.3897 69.4391 54.8654 71.846C53.502 72.7695 52.7259 74.1316 52.6903 75.6827C52.6405 77.6117 53.8081 79.498 55.1217 81.017C49.9314 85.1639 45.7343 89.1825 44.2462 92.2811C42.5873 96.0893 41.9109 102.322 45.008 108.402C48.9382 116.118 57.6279 121.499 70.8423 124.394C88.1114 128.17 103.027 124.768 112.895 119.566C118.388 116.671 122.286 113.215 124.18 110.131C124.768 110.317 125.355 110.506 125.96 110.695C126.804 110.951 127.648 111.208 128.438 111.49C131.051 112.395 133.942 112.274 136.167 111.151C136.206 111.133 136.248 111.108 136.291 111.087C137.968 110.202 139.175 108.783 139.705 107.072C141.129 102.458 137.064 99.9082 134.614 98.3714ZM64.9999 90.6681C63.4307 90.6681 62.1621 91.9382 62.1621 93.5091C62.1621 95.0836 63.4307 96.3537 64.9999 96.3537C66.5691 96.3537 67.8378 95.0836 67.8378 93.5091C67.8378 91.9382 66.5691 90.6681 64.9999 90.6681ZM91.7568 99.1965C90.1876 99.1965 88.9189 100.467 88.9189 102.038C88.9189 103.612 90.1876 104.882 91.7568 104.882C93.326 104.882 94.5946 103.612 94.5946 102.038C94.5946 100.467 93.326 99.1965 91.7568 99.1965Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000">
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Discord-Logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.644 96"><path id="Discord-Symbol-Black" d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1,104 @@
<svg version="1.1" viewBox="0 0 268.1522 273.8827" overflow="hidden" xml:space="preserve" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="a">
<stop offset="0" stop-color="#0fbc5c"/>
<stop offset="1" stop-color="#0cba65"/>
</linearGradient>
<linearGradient id="g">
<stop offset=".2312727" stop-color="#0fbc5f"/>
<stop offset=".3115468" stop-color="#0fbc5f"/>
<stop offset=".3660131" stop-color="#0fbc5e"/>
<stop offset=".4575163" stop-color="#0fbc5d"/>
<stop offset=".540305" stop-color="#12bc58"/>
<stop offset=".6993464" stop-color="#28bf3c"/>
<stop offset=".7712418" stop-color="#38c02b"/>
<stop offset=".8605665" stop-color="#52c218"/>
<stop offset=".9150327" stop-color="#67c30f"/>
<stop offset="1" stop-color="#86c504"/>
</linearGradient>
<linearGradient id="h">
<stop offset=".1416122" stop-color="#1abd4d"/>
<stop offset=".2475151" stop-color="#6ec30d"/>
<stop offset=".3115468" stop-color="#8ac502"/>
<stop offset=".3660131" stop-color="#a2c600"/>
<stop offset=".4456735" stop-color="#c8c903"/>
<stop offset=".540305" stop-color="#ebcb03"/>
<stop offset=".6156363" stop-color="#f7cd07"/>
<stop offset=".6993454" stop-color="#fdcd04"/>
<stop offset=".7712418" stop-color="#fdce05"/>
<stop offset=".8605661" stop-color="#ffce0a"/>
</linearGradient>
<linearGradient id="f">
<stop offset=".3159041" stop-color="#ff4c3c"/>
<stop offset=".6038179" stop-color="#ff692c"/>
<stop offset=".7268366" stop-color="#ff7825"/>
<stop offset=".884534" stop-color="#ff8d1b"/>
<stop offset="1" stop-color="#ff9f13"/>
</linearGradient>
<linearGradient id="b">
<stop offset=".2312727" stop-color="#ff4541"/>
<stop offset=".3115468" stop-color="#ff4540"/>
<stop offset=".4575163" stop-color="#ff4640"/>
<stop offset=".540305" stop-color="#ff473f"/>
<stop offset=".6993464" stop-color="#ff5138"/>
<stop offset=".7712418" stop-color="#ff5b33"/>
<stop offset=".8605665" stop-color="#ff6c29"/>
<stop offset="1" stop-color="#ff8c18"/>
</linearGradient>
<linearGradient id="d">
<stop offset=".4084578" stop-color="#fb4e5a"/>
<stop offset="1" stop-color="#ff4540"/>
</linearGradient>
<linearGradient id="c">
<stop offset=".1315461" stop-color="#0cba65"/>
<stop offset=".2097843" stop-color="#0bb86d"/>
<stop offset=".2972969" stop-color="#09b479"/>
<stop offset=".3962575" stop-color="#08ad93"/>
<stop offset=".4771242" stop-color="#0aa6a9"/>
<stop offset=".5684245" stop-color="#0d9cc6"/>
<stop offset=".667385" stop-color="#1893dd"/>
<stop offset=".7687273" stop-color="#258bf1"/>
<stop offset=".8585063" stop-color="#3086ff"/>
</linearGradient>
<linearGradient id="e">
<stop offset=".3660131" stop-color="#ff4e3a"/>
<stop offset=".4575163" stop-color="#ff8a1b"/>
<stop offset=".540305" stop-color="#ffa312"/>
<stop offset=".6156363" stop-color="#ffb60c"/>
<stop offset=".7712418" stop-color="#ffcd0a"/>
<stop offset=".8605665" stop-color="#fecf0a"/>
<stop offset=".9150327" stop-color="#fecf08"/>
<stop offset="1" stop-color="#fdcd01"/>
</linearGradient>
<linearGradient xlink:href="#a" id="s" x1="219.6997" y1="329.5351" x2="254.4673" y2="329.5351" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#b" id="m" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.936885,1.043001,1.455731,2.555422,290.5254,-400.6338)" cx="109.6267" cy="135.8619" fx="109.6267" fy="135.8619" r="71.46001"/>
<radialGradient xlink:href="#c" id="n" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.512595,-4.45809,-1.692547,1.260616,870.8006,191.554)" cx="45.25866" cy="279.2738" fx="45.25866" fy="279.2738" r="71.46001"/>
<radialGradient xlink:href="#d" id="l" cx="304.0166" cy="118.0089" fx="304.0166" fy="118.0089" r="47.85445" gradientTransform="matrix(2.064353,-4.926832e-6,-2.901531e-6,2.592041,-297.6788,-151.7469)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#e" id="o" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.2485783,2.083138,2.962486,0.3341668,-255.1463,-331.1636)" cx="181.001" cy="177.2013" fx="181.001" fy="177.2013" r="71.46001"/>
<radialGradient xlink:href="#f" id="p" cx="207.6733" cy="108.0972" fx="207.6733" fy="108.0972" r="41.1025" gradientTransform="matrix(-1.249206,1.343263,-3.896837,-3.425693,880.5011,194.9051)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#g" id="r" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.936885,-1.043001,1.455731,-2.555422,290.5254,838.6834)" cx="109.6267" cy="135.8619" fx="109.6267" fy="135.8619" r="71.46001"/>
<radialGradient xlink:href="#h" id="j" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.081402,-1.93722,2.926737,-0.1162508,-215.1345,632.8606)" cx="154.8697" cy="145.9691" fx="154.8697" fy="145.9691" r="71.46001"/>
<filter id="q" x="-.04842873" y="-.0582241" width="1.096857" height="1.116448" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="1.700914"/>
</filter>
<filter id="k" x="-.01670084" y="-.01009856" width="1.033402" height="1.020197" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation=".2419367"/>
</filter>
<clipPath clipPathUnits="userSpaceOnUse" id="i">
<path d="M371.3784 193.2406H237.0825v53.4375h77.167c-1.2405 7.5627-4.0259 15.0024-8.1049 21.7862-4.6734 7.7723-10.4511 13.6895-16.373 18.1957-17.7389 13.4983-38.42 16.2584-52.7828 16.2584-36.2824 0-67.2833-23.2865-79.2844-54.9287-.4843-1.1482-.8059-2.3344-1.1975-3.5068-2.652-8.0533-4.101-16.5825-4.101-25.4474 0-9.226 1.5691-18.0575 4.4301-26.3985 11.2851-32.8967 42.9849-57.4674 80.1789-57.4674 7.4811 0 14.6854.8843 21.5173 2.6481 15.6135 4.0309 26.6578 11.9698 33.4252 18.2494l40.834-39.7111c-24.839-22.616-57.2194-36.3201-95.8444-36.3201-30.8782-.00066-59.3863 9.55308-82.7477 25.6992-18.9454 13.0941-34.4833 30.6254-44.9695 50.9861-9.75366 18.8785-15.09441 39.7994-15.09441 62.2934 0 22.495 5.34891 43.6334 15.10261 62.3374v.126c10.3023 19.8567 25.3678 36.9537 43.6783 49.9878 15.9962 11.3866 44.6789 26.5516 84.0307 26.5516 22.6301 0 42.6867-4.0517 60.3748-11.6447 12.76-5.4775 24.0655-12.6217 34.3012-21.8036 13.5247-12.1323 24.1168-27.1388 31.3465-44.4041 7.2297-17.2654 11.097-36.7895 11.097-57.957 0-9.858-.9971-19.8694-2.6881-28.9684Z" fill="#000"/>
</clipPath>
</defs>
<g transform="matrix(0.957922,0,0,0.985255,-90.17436,-78.85577)">
<g clip-path="url(#i)">
<path d="M92.07563 219.9585c.14844 22.14 6.5014 44.983 16.11767 63.4234v.1269c6.9482 13.3919 16.4444 23.9704 27.2604 34.4518l65.326-23.67c-12.3593-6.2344-14.2452-10.0546-23.1048-17.0253-9.0537-9.0658-15.8015-19.4735-20.0038-31.677h-.1693l.1693-.1269c-2.7646-8.0587-3.0373-16.6129-3.1393-25.5029Z" fill="url(#j)" filter="url(#k)"/>
<path d="M237.0835 79.02491c-6.4568 22.52569-3.988 44.42139 0 57.16129 7.4561.0055 14.6388.8881 21.4494 2.6464 15.6135 4.0309 26.6566 11.97 33.424 18.2496l41.8794-40.7256c-24.8094-22.58904-54.6663-37.2961-96.7528-37.33169Z" fill="url(#l)" filter="url(#k)"/>
<path d="M236.9434 78.84678c-31.6709-.00068-60.9107 9.79833-84.8718 26.35902-8.8968 6.149-17.0612 13.2521-24.3311 21.1509-1.9045 17.7429 14.2569 39.5507 46.2615 39.3702 15.5284-17.9373 38.4946-29.5427 64.0561-29.5427.0233 0 .046.0019.0693.002l-1.0439-57.33536c-.0472-.00003-.0929-.00406-.1401-.00406Z" fill="url(#m)" filter="url(#k)"/>
<path d="m341.4751 226.3788-28.2685 19.2848c-1.2405 7.5627-4.0278 15.0023-8.1068 21.7861-4.6734 7.7723-10.4506 13.6898-16.3725 18.196-17.7022 13.4704-38.3286 16.2439-52.6877 16.2553-14.8415 25.1018-17.4435 37.6749 1.0439 57.9342 22.8762-.0167 43.157-4.1174 61.0458-11.7965 12.9312-5.551 24.3879-12.7913 34.7609-22.0964 13.7061-12.295 24.4421-27.5034 31.7688-45.0003 7.3267-17.497 11.2446-37.2822 11.2446-58.7336Z" fill="url(#n)" filter="url(#k)"/>
<path d="M234.9956 191.2104v57.4981h136.0062c1.1962-7.8745 5.1523-18.0644 5.1523-26.5001 0-9.858-.9963-21.899-2.6873-30.998Z" fill="#3086ff" filter="url(#k)"/>
<path d="M128.3894 124.3268c-8.393 9.1191-15.5632 19.326-21.2483 30.3646-9.75351 18.8785-15.09402 41.8295-15.09402 64.3235 0 .317.02642.6271.02855.9436 4.31953 8.2244 59.66647 6.6495 62.45617 0-.0035-.3103-.0387-.6128-.0387-.9238 0-9.226 1.5696-16.0262 4.4306-24.3672 3.5294-10.2885 9.0557-19.7628 16.1223-27.9257 1.6019-2.0309 5.8748-6.3969 7.1214-9.0157.4749-.9975-.8621-1.5574-.9369-1.9085-.0836-.3927-1.8762-.0769-2.2778-.3694-1.2751-.9288-3.8001-1.4138-5.3334-1.8449-3.2772-.9215-8.7085-2.9536-11.7252-5.0601-9.5357-6.6586-24.417-14.6122-33.5047-24.2164Z" fill="url(#o)" filter="url(#k)"/>
<path d="M162.0989 155.8569c22.1123 13.3013 28.4714-6.7139 43.173-12.9771L179.698 90.21568c-9.4075 3.92642-18.2957 8.80465-26.5426 14.50442-12.316 8.5122-23.192 18.8995-32.1763 30.7204Z" fill="url(#p)" filter="url(#q)"/>
<path d="M171.0987 290.222c-29.6829 10.6413-34.3299 11.023-37.0622 29.2903 5.2213 5.0597 10.8312 9.74 16.7926 13.9835 15.9962 11.3867 46.766 26.5517 86.1178 26.5517.0462 0 .0904-.004.1366-.004v-59.1574c-.0298.0001-.064.002-.0938.002-14.7359 0-26.5113-3.8435-38.5848-10.5273-2.9768-1.6479-8.3775 2.7772-11.1229.799-3.7865-2.7284-12.8991 2.3508-16.1833-.9378Z" fill="url(#r)" filter="url(#k)"/>
<path d="M219.6997 299.0227v59.9959c5.506.6402 11.2361 1.0289 17.2472 1.0289 6.0259 0 11.8556-.3073 17.5204-.8723v-59.7481c-6.3482 1.0777-12.3272 1.461-17.4776 1.461-5.9318 0-11.7005-.6858-17.29-1.8654Z" opacity=".5" fill="url(#s)" filter="url(#k)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><path fill="#f35325" d="M0 0h10v10H0z"/><path fill="#81bc06" d="M11 0h10v10H11z"/><path fill="#05a6f0" d="M0 11h10v10H0z"/><path fill="#ffba08" d="M11 11h10v10H11z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

View File

@ -40,37 +40,37 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (11.10.0):
- FirebaseCore (~> 11.10.0)
- Firebase/Messaging (11.10.0):
- Firebase/CoreOnly (11.13.0):
- FirebaseCore (~> 11.13.0)
- Firebase/Messaging (11.13.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.10.0)
- firebase_core (3.13.1):
- Firebase/CoreOnly (= 11.10.0)
- FirebaseMessaging (~> 11.13.0)
- firebase_core (3.14.0):
- Firebase/CoreOnly (= 11.13.0)
- Flutter
- firebase_messaging (15.2.6):
- Firebase/Messaging (= 11.10.0)
- firebase_messaging (15.2.7):
- Firebase/Messaging (= 11.13.0)
- firebase_core
- Flutter
- FirebaseCore (11.10.0):
- FirebaseCoreInternal (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.10.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.10.0):
- FirebaseCore (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- FirebaseCore (11.13.0):
- FirebaseCoreInternal (~> 11.13.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.13.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.13.0):
- FirebaseCore (~> 11.13.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.10.0):
- FirebaseCore (~> 11.10.0)
- FirebaseMessaging (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0)
- flutter_inappwebview_ios (0.0.1):
@ -84,6 +84,8 @@ PODS:
- Flutter
- flutter_platform_alert (0.0.1):
- Flutter
- flutter_timezone (0.0.1):
- Flutter
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
@ -92,6 +94,7 @@ PODS:
- WebRTC-SDK (= 125.6422.07)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
@ -124,7 +127,7 @@ PODS:
- irondash_engine_context (0.0.1):
- Flutter
- Kingfisher (8.3.2)
- livekit_client (2.4.7):
- livekit_client (2.4.8):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.07)
@ -137,6 +140,8 @@ PODS:
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- native_exif (0.0.1):
- Flutter
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
@ -149,32 +154,34 @@ PODS:
- record_ios (1.0.0):
- Flutter
- SAMKeychain (1.5.3)
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.2):
- sqlite3/common (= 3.49.2)
- sqlite3/common (3.49.2)
- sqlite3/dbstatvtab (3.49.2):
- sqlite3 (3.50.1):
- sqlite3/common (= 3.50.1)
- sqlite3/common (3.50.1)
- sqlite3/dbstatvtab (3.50.1):
- sqlite3/common
- sqlite3/fts5 (3.49.2):
- sqlite3/fts5 (3.50.1):
- sqlite3/common
- sqlite3/math (3.49.2):
- sqlite3/math (3.50.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.2):
- sqlite3/perf-threadsafe (3.50.1):
- sqlite3/common
- sqlite3/rtree (3.49.2):
- sqlite3/rtree (3.50.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.2)
- sqlite3 (~> 3.50.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@ -203,20 +210,23 @@ DEPENDENCIES:
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- native_exif (from `.symlinks/plugins/native_exif/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
@ -267,12 +277,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_platform_alert:
:path: ".symlinks/plugins/flutter_platform_alert/ios"
flutter_timezone:
:path: ".symlinks/plugins/flutter_timezone/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/ios"
:path: ".symlinks/plugins/gal/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
irondash_engine_context:
@ -283,6 +295,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
native_exif:
:path: ".symlinks/plugins/native_exif/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
@ -293,6 +307,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/record_ios/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
@ -314,29 +330,31 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_core: ba71b44041571da878cb624ce0d80250bcbe58ad
firebase_messaging: 13129fe2ca166d1ed2d095062d76cee88943d067
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3
FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9
Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327
firebase_core: 700bac7ed92bb754fd70fbf01d72b36ecdd6d450
firebase_messaging: 860c017fcfbb5e27c163062d1d3135388f3ef954
FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0
FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117
gal: 29e711cd17bccb47f839d9b30afe9bc9750950b2
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5
livekit_client: c30950bf36aa4c0244dd5551b1818cd15f90ba32
livekit_client: 9e901890552514206e5ff828903ed271531da264
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
@ -344,11 +362,12 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImage: f29024626962457f3470184232766516dee8dfea
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
sqlite3_flutter_libs: 74334e3ef2dbdb7d37e50859bb45da43935779c4
sqlite3: 1d85290c3321153511f6e900ede7a1608718bbd5
sqlite3_flutter_libs: e7fc8c9ea2200ff3271f08f127842131746b70e2
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d

View File

@ -2,6 +2,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>dev.solsynth.solian</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>

View File

@ -4,6 +4,16 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:solian.app</string>
</array>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
</dict>

View File

@ -1,6 +1,3 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:island/database/drift_db.dart';
import 'package:island/database/message.dart';
@ -48,13 +45,6 @@ class MessageRepository {
},
);
for (final item in resp.data['changes']) {
if (item['message']['sender']['account'] == null)
log(jsonEncode(item['message']['sender']['account']));
// if (item['message'] != null &&
// item['message']['sender']['account'] == null) {
// }
}
final response = MessageSyncResponse.fromJson(resp.data);
for (final change in response.changes) {
switch (change.action) {

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:croppy/croppy.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@ -17,14 +18,15 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/screens/auth/tabs.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/timezone.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:relative_time/relative_time.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher_string.dart';
void main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
@ -45,6 +47,14 @@ void main() async {
showErrorAlert(err);
}
try {
log("[SplashScreen] Loading timezone database...");
await initializeTzdb();
log("[SplashScreen] Time zone database was loaded!");
} catch (err) {
log("[SplashScreen] Failed to load timezone database... $err");
}
final prefs = await SharedPreferences.getInstance();
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
@ -93,7 +103,7 @@ void main() async {
);
}
final _appRouter = AppRouter();
final appRouter = AppRouter();
class IslandApp extends HookConsumerWidget {
const IslandApp({super.key});
@ -102,6 +112,33 @@ class IslandApp extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themeProvider);
void handleMessage(RemoteMessage notification) {
if (notification.data['action_uri'] != null) {
var uri = notification.data['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
appRouter.pushPath(notification.data['action_uri']);
} else {
// External links
launchUrlString(uri);
}
}
}
useEffect(() {
Future(() async {
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
handleMessage(initialMessage);
}
FirebaseMessaging.onMessageOpenedApp.listen(handleMessage);
});
return null;
}, []);
useEffect(() {
// Load userinfo
final userNotifier = ref.read(userInfoProvider.notifier);
@ -126,22 +163,13 @@ class IslandApp extends HookConsumerWidget {
theme: theme?.light,
darkTheme: theme?.dark,
themeMode: ThemeMode.system,
routerConfig: _appRouter.config(
navigatorObservers:
() => [
TabNavigationObserver(
onChange: (route) {
ref.read(currentRouteProvider.notifier).state = route;
},
),
],
),
routerConfig: appRouter.config(),
supportedLocales: context.supportedLocales,
localizationsDelegates: [
...context.localizationDelegates,
CroppyLocalizations.delegate,
RelativeTimeLocalizations.delegate,
], // this contains the cupertino one
],
locale: context.locale,
builder: (context, child) {
return Overlay(
@ -149,11 +177,8 @@ class IslandApp extends HookConsumerWidget {
OverlayEntry(
builder:
(_) => WindowScaffold(
router: _appRouter,
child: TabsNavigationWidget(
router: _appRouter,
child: child ?? const SizedBox.shrink(),
),
router: appRouter,
child: child ?? const SizedBox.shrink(),
),
),
],

View File

@ -91,3 +91,21 @@ sealed class SnAuthDevice with _$SnAuthDevice {
factory SnAuthDevice.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceFromJson(json);
}
@freezed
sealed class SnAccountConnection with _$SnAccountConnection {
const factory SnAccountConnection({
required String id,
required String accountId,
required String provider,
required String providedIdentifier,
@Default({}) Map<String, dynamic> meta,
required DateTime lastUsedAt,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAccountConnection;
factory SnAccountConnection.fromJson(Map<String, dynamic> json) =>
_$SnAccountConnectionFromJson(json);
}

View File

@ -847,6 +847,169 @@ as bool,
}
}
/// @nodoc
mixin _$SnAccountConnection {
String get id; String get accountId; String get provider; String get providedIdentifier; Map<String, dynamic> get meta; DateTime get lastUsedAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountConnection
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAccountConnectionCopyWith<SnAccountConnection> get copyWith => _$SnAccountConnectionCopyWithImpl<SnAccountConnection>(this as SnAccountConnection, _$identity);
/// Serializes this SnAccountConnection to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountConnection&&(identical(other.id, id) || other.id == id)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.providedIdentifier, providedIdentifier) || other.providedIdentifier == providedIdentifier)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.lastUsedAt, lastUsedAt) || other.lastUsedAt == lastUsedAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,accountId,provider,providedIdentifier,const DeepCollectionEquality().hash(meta),lastUsedAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountConnection(id: $id, accountId: $accountId, provider: $provider, providedIdentifier: $providedIdentifier, meta: $meta, lastUsedAt: $lastUsedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAccountConnectionCopyWith<$Res> {
factory $SnAccountConnectionCopyWith(SnAccountConnection value, $Res Function(SnAccountConnection) _then) = _$SnAccountConnectionCopyWithImpl;
@useResult
$Res call({
String id, String accountId, String provider, String providedIdentifier, Map<String, dynamic> meta, DateTime lastUsedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnAccountConnectionCopyWithImpl<$Res>
implements $SnAccountConnectionCopyWith<$Res> {
_$SnAccountConnectionCopyWithImpl(this._self, this._then);
final SnAccountConnection _self;
final $Res Function(SnAccountConnection) _then;
/// Create a copy of SnAccountConnection
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? accountId = null,Object? provider = null,Object? providedIdentifier = null,Object? meta = null,Object? lastUsedAt = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable
as String,providedIdentifier: null == providedIdentifier ? _self.providedIdentifier : providedIdentifier // ignore: cast_nullable_to_non_nullable
as String,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,lastUsedAt: null == lastUsedAt ? _self.lastUsedAt : lastUsedAt // ignore: cast_nullable_to_non_nullable
as DateTime,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnAccountConnection implements SnAccountConnection {
const _SnAccountConnection({required this.id, required this.accountId, required this.provider, required this.providedIdentifier, final Map<String, dynamic> meta = const {}, required this.lastUsedAt, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta;
factory _SnAccountConnection.fromJson(Map<String, dynamic> json) => _$SnAccountConnectionFromJson(json);
@override final String id;
@override final String accountId;
@override final String provider;
@override final String providedIdentifier;
final Map<String, dynamic> _meta;
@override@JsonKey() Map<String, dynamic> get meta {
if (_meta is EqualUnmodifiableMapView) return _meta;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_meta);
}
@override final DateTime lastUsedAt;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAccountConnection
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAccountConnectionCopyWith<_SnAccountConnection> get copyWith => __$SnAccountConnectionCopyWithImpl<_SnAccountConnection>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAccountConnectionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountConnection&&(identical(other.id, id) || other.id == id)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.providedIdentifier, providedIdentifier) || other.providedIdentifier == providedIdentifier)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.lastUsedAt, lastUsedAt) || other.lastUsedAt == lastUsedAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,accountId,provider,providedIdentifier,const DeepCollectionEquality().hash(_meta),lastUsedAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountConnection(id: $id, accountId: $accountId, provider: $provider, providedIdentifier: $providedIdentifier, meta: $meta, lastUsedAt: $lastUsedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAccountConnectionCopyWith<$Res> implements $SnAccountConnectionCopyWith<$Res> {
factory _$SnAccountConnectionCopyWith(_SnAccountConnection value, $Res Function(_SnAccountConnection) _then) = __$SnAccountConnectionCopyWithImpl;
@override @useResult
$Res call({
String id, String accountId, String provider, String providedIdentifier, Map<String, dynamic> meta, DateTime lastUsedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnAccountConnectionCopyWithImpl<$Res>
implements _$SnAccountConnectionCopyWith<$Res> {
__$SnAccountConnectionCopyWithImpl(this._self, this._then);
final _SnAccountConnection _self;
final $Res Function(_SnAccountConnection) _then;
/// Create a copy of SnAccountConnection
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? accountId = null,Object? provider = null,Object? providedIdentifier = null,Object? meta = null,Object? lastUsedAt = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountConnection(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable
as String,providedIdentifier: null == providedIdentifier ? _self.providedIdentifier : providedIdentifier // ignore: cast_nullable_to_non_nullable
as String,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,lastUsedAt: null == lastUsedAt ? _self.lastUsedAt : lastUsedAt // ignore: cast_nullable_to_non_nullable
as DateTime,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

@ -155,3 +155,33 @@ Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) =>
'sessions': instance.sessions.map((e) => e.toJson()).toList(),
'is_current': instance.isCurrent,
};
_SnAccountConnection _$SnAccountConnectionFromJson(Map<String, dynamic> json) =>
_SnAccountConnection(
id: json['id'] as String,
accountId: json['account_id'] as String,
provider: json['provider'] as String,
providedIdentifier: json['provided_identifier'] as String,
meta: json['meta'] as Map<String, dynamic>? ?? const {},
lastUsedAt: DateTime.parse(json['last_used_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnAccountConnectionToJson(
_SnAccountConnection instance,
) => <String, dynamic>{
'id': instance.id,
'account_id': instance.accountId,
'provider': instance.provider,
'provided_identifier': instance.providedIdentifier,
'meta': instance.meta,
'last_used_at': instance.lastUsedAt.toIso8601String(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -1,6 +1,3 @@
import 'dart:convert';
import 'dart:developer';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
@ -90,7 +87,10 @@ sealed class SnChatMember with _$SnChatMember {
required int role,
required int notify,
required DateTime? joinedAt,
required DateTime? breakUntil,
required DateTime? timeoutUntil,
required bool isBot,
// Frontend data
DateTime? lastTyped,
}) = _SnChatMember;

View File

@ -663,7 +663,8 @@ $SnChatMemberCopyWith<$Res> get sender {
/// @nodoc
mixin _$SnChatMember {
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get chatRoomId; SnChatRoom? get chatRoom; String get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot; DateTime? get lastTyped;
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get chatRoomId; SnChatRoom? get chatRoom; String get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; DateTime? get breakUntil; DateTime? get timeoutUntil; bool get isBot;// Frontend data
DateTime? get lastTyped;
/// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -676,16 +677,16 @@ $SnChatMemberCopyWith<SnChatMember> get copyWith => _$SnChatMemberCopyWithImpl<S
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.breakUntil, breakUntil) || other.breakUntil == breakUntil)&&(identical(other.timeoutUntil, timeoutUntil) || other.timeoutUntil == timeoutUntil)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped);
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,breakUntil,timeoutUntil,isBot,lastTyped);
@override
String toString() {
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot, lastTyped: $lastTyped)';
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, breakUntil: $breakUntil, timeoutUntil: $timeoutUntil, isBot: $isBot, lastTyped: $lastTyped)';
}
@ -696,7 +697,7 @@ abstract mixin class $SnChatMemberCopyWith<$Res> {
factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl;
@useResult
$Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot, DateTime? lastTyped
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, DateTime? breakUntil, DateTime? timeoutUntil, bool isBot, DateTime? lastTyped
});
@ -713,7 +714,7 @@ class _$SnChatMemberCopyWithImpl<$Res>
/// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,Object? lastTyped = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? breakUntil = freezed,Object? timeoutUntil = freezed,Object? isBot = null,Object? lastTyped = freezed,}) {
return _then(_self.copyWith(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -727,6 +728,8 @@ as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_
as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,breakUntil: freezed == breakUntil ? _self.breakUntil : breakUntil // ignore: cast_nullable_to_non_nullable
as DateTime?,timeoutUntil: freezed == timeoutUntil ? _self.timeoutUntil : timeoutUntil // ignore: cast_nullable_to_non_nullable
as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable
as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable
as DateTime?,
@ -761,7 +764,7 @@ $SnAccountCopyWith<$Res> get account {
@JsonSerializable()
class _SnChatMember implements SnChatMember {
const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.chatRoom, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.isBot, this.lastTyped});
const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.chatRoom, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.breakUntil, required this.timeoutUntil, required this.isBot, this.lastTyped});
factory _SnChatMember.fromJson(Map<String, dynamic> json) => _$SnChatMemberFromJson(json);
@override final DateTime createdAt;
@ -776,7 +779,10 @@ class _SnChatMember implements SnChatMember {
@override final int role;
@override final int notify;
@override final DateTime? joinedAt;
@override final DateTime? breakUntil;
@override final DateTime? timeoutUntil;
@override final bool isBot;
// Frontend data
@override final DateTime? lastTyped;
/// Create a copy of SnChatMember
@ -792,16 +798,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.breakUntil, breakUntil) || other.breakUntil == breakUntil)&&(identical(other.timeoutUntil, timeoutUntil) || other.timeoutUntil == timeoutUntil)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped);
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,breakUntil,timeoutUntil,isBot,lastTyped);
@override
String toString() {
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot, lastTyped: $lastTyped)';
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, breakUntil: $breakUntil, timeoutUntil: $timeoutUntil, isBot: $isBot, lastTyped: $lastTyped)';
}
@ -812,7 +818,7 @@ abstract mixin class _$SnChatMemberCopyWith<$Res> implements $SnChatMemberCopyWi
factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl;
@override @useResult
$Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot, DateTime? lastTyped
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, DateTime? breakUntil, DateTime? timeoutUntil, bool isBot, DateTime? lastTyped
});
@ -829,7 +835,7 @@ class __$SnChatMemberCopyWithImpl<$Res>
/// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,Object? lastTyped = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? breakUntil = freezed,Object? timeoutUntil = freezed,Object? isBot = null,Object? lastTyped = freezed,}) {
return _then(_SnChatMember(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -843,6 +849,8 @@ as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_
as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,breakUntil: freezed == breakUntil ? _self.breakUntil : breakUntil // ignore: cast_nullable_to_non_nullable
as DateTime?,timeoutUntil: freezed == timeoutUntil ? _self.timeoutUntil : timeoutUntil // ignore: cast_nullable_to_non_nullable
as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable
as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable
as DateTime?,

View File

@ -166,6 +166,14 @@ _SnChatMember _$SnChatMemberFromJson(Map<String, dynamic> json) =>
json['joined_at'] == null
? null
: DateTime.parse(json['joined_at'] as String),
breakUntil:
json['break_until'] == null
? null
: DateTime.parse(json['break_until'] as String),
timeoutUntil:
json['timeout_until'] == null
? null
: DateTime.parse(json['timeout_until'] as String),
isBot: json['is_bot'] as bool,
lastTyped:
json['last_typed'] == null
@ -187,6 +195,8 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
'role': instance.role,
'notify': instance.notify,
'joined_at': instance.joinedAt?.toIso8601String(),
'break_until': instance.breakUntil?.toIso8601String(),
'timeout_until': instance.timeoutUntil?.toIso8601String(),
'is_bot': instance.isBot,
'last_typed': instance.lastTyped?.toIso8601String(),
};

23
lib/models/embed.dart Normal file
View File

@ -0,0 +1,23 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'embed.freezed.dart';
part 'embed.g.dart';
@freezed
sealed class SnEmbedLink with _$SnEmbedLink {
const factory SnEmbedLink({
@JsonKey(name: 'Type') required String type,
@JsonKey(name: 'Url') required String url,
@JsonKey(name: 'Title') required String title,
@JsonKey(name: 'Description') required String? description,
@JsonKey(name: 'ImageUrl') required String? imageUrl,
@JsonKey(name: 'FaviconUrl') required String faviconUrl,
@JsonKey(name: 'SiteName') required String siteName,
@JsonKey(name: 'ContentType') required String? contentType,
@JsonKey(name: 'Author') required String? author,
@JsonKey(name: 'PublishedDate') required DateTime? publishedDate,
}) = _SnEmbedLink;
factory SnEmbedLink.fromJson(Map<String, dynamic> json) =>
_$SnEmbedLinkFromJson(json);
}

View File

@ -0,0 +1,175 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'embed.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnEmbedLink {
@JsonKey(name: 'Type') String get type;@JsonKey(name: 'Url') String get url;@JsonKey(name: 'Title') String get title;@JsonKey(name: 'Description') String? get description;@JsonKey(name: 'ImageUrl') String? get imageUrl;@JsonKey(name: 'FaviconUrl') String get faviconUrl;@JsonKey(name: 'SiteName') String get siteName;@JsonKey(name: 'ContentType') String? get contentType;@JsonKey(name: 'Author') String? get author;@JsonKey(name: 'PublishedDate') DateTime? get publishedDate;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnEmbedLinkCopyWith<SnEmbedLink> get copyWith => _$SnEmbedLinkCopyWithImpl<SnEmbedLink>(this as SnEmbedLink, _$identity);
/// Serializes this SnEmbedLink to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
@override
String toString() {
return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
}
}
/// @nodoc
abstract mixin class $SnEmbedLinkCopyWith<$Res> {
factory $SnEmbedLinkCopyWith(SnEmbedLink value, $Res Function(SnEmbedLink) _then) = _$SnEmbedLinkCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate
});
}
/// @nodoc
class _$SnEmbedLinkCopyWithImpl<$Res>
implements $SnEmbedLinkCopyWith<$Res> {
_$SnEmbedLinkCopyWithImpl(this._self, this._then);
final SnEmbedLink _self;
final $Res Function(SnEmbedLink) _then;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnEmbedLink implements SnEmbedLink {
const _SnEmbedLink({@JsonKey(name: 'Type') required this.type, @JsonKey(name: 'Url') required this.url, @JsonKey(name: 'Title') required this.title, @JsonKey(name: 'Description') required this.description, @JsonKey(name: 'ImageUrl') required this.imageUrl, @JsonKey(name: 'FaviconUrl') required this.faviconUrl, @JsonKey(name: 'SiteName') required this.siteName, @JsonKey(name: 'ContentType') required this.contentType, @JsonKey(name: 'Author') required this.author, @JsonKey(name: 'PublishedDate') required this.publishedDate});
factory _SnEmbedLink.fromJson(Map<String, dynamic> json) => _$SnEmbedLinkFromJson(json);
@override@JsonKey(name: 'Type') final String type;
@override@JsonKey(name: 'Url') final String url;
@override@JsonKey(name: 'Title') final String title;
@override@JsonKey(name: 'Description') final String? description;
@override@JsonKey(name: 'ImageUrl') final String? imageUrl;
@override@JsonKey(name: 'FaviconUrl') final String faviconUrl;
@override@JsonKey(name: 'SiteName') final String siteName;
@override@JsonKey(name: 'ContentType') final String? contentType;
@override@JsonKey(name: 'Author') final String? author;
@override@JsonKey(name: 'PublishedDate') final DateTime? publishedDate;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnEmbedLinkCopyWith<_SnEmbedLink> get copyWith => __$SnEmbedLinkCopyWithImpl<_SnEmbedLink>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnEmbedLinkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
@override
String toString() {
return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
}
}
/// @nodoc
abstract mixin class _$SnEmbedLinkCopyWith<$Res> implements $SnEmbedLinkCopyWith<$Res> {
factory _$SnEmbedLinkCopyWith(_SnEmbedLink value, $Res Function(_SnEmbedLink) _then) = __$SnEmbedLinkCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate
});
}
/// @nodoc
class __$SnEmbedLinkCopyWithImpl<$Res>
implements _$SnEmbedLinkCopyWith<$Res> {
__$SnEmbedLinkCopyWithImpl(this._self, this._then);
final _SnEmbedLink _self;
final $Res Function(_SnEmbedLink) _then;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
return _then(_SnEmbedLink(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

37
lib/models/embed.g.dart Normal file
View File

@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'embed.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnEmbedLink _$SnEmbedLinkFromJson(Map<String, dynamic> json) => _SnEmbedLink(
type: json['Type'] as String,
url: json['Url'] as String,
title: json['Title'] as String,
description: json['Description'] as String?,
imageUrl: json['ImageUrl'] as String?,
faviconUrl: json['FaviconUrl'] as String,
siteName: json['SiteName'] as String,
contentType: json['ContentType'] as String?,
author: json['Author'] as String?,
publishedDate:
json['PublishedDate'] == null
? null
: DateTime.parse(json['PublishedDate'] as String),
);
Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) =>
<String, dynamic>{
'Type': instance.type,
'Url': instance.url,
'Title': instance.title,
'Description': instance.description,
'ImageUrl': instance.imageUrl,
'FaviconUrl': instance.faviconUrl,
'SiteName': instance.siteName,
'ContentType': instance.contentType,
'Author': instance.author,
'PublishedDate': instance.publishedDate?.toIso8601String(),
};

View File

@ -22,6 +22,7 @@ sealed class SnPost with _$SnPost {
required int viewsTotal,
required int upvotes,
required int downvotes,
required int repliesCount,
required String? threadedPostId,
required SnPost? threadedPost,
required String? repliedPostId,
@ -38,6 +39,7 @@ sealed class SnPost with _$SnPost {
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
@Default(false) bool isTruncated,
}) = _SnPost;
factory SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@ -59,6 +61,7 @@ sealed class SnPublisher with _$SnPublisher {
required DateTime updatedAt,
required DateTime? deletedAt,
required String? realmId,
required SnVerificationMark? verification,
}) = _SnPublisher;
factory SnPublisher.fromJson(Map<String, dynamic> json) =>
@ -83,7 +86,7 @@ sealed class SnPublisherStats with _$SnPublisherStats {
sealed class SnSubscriptionStatus with _$SnSubscriptionStatus {
const factory SnSubscriptionStatus({
required bool isSubscribed,
required int publisherId,
required String publisherId,
required String publisherName,
}) = _SnSubscriptionStatus;

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPost {
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<dynamic> get tags; List<dynamic> get categories; List<dynamic> get collections; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<dynamic> get tags; List<dynamic> get categories; List<dynamic> get collections; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; bool get isTruncated;
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@ -49,7 +49,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
@useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, bool isTruncated
});
@ -66,7 +66,7 @@ class _$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@ -82,6 +82,7 @@ as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique :
as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable
as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable
as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable
@ -98,7 +99,8 @@ as List<dynamic>,collections: null == collections ? _self.collections : collecti
as List<dynamic>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
as DateTime?,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable
as bool,
));
}
/// Create a copy of SnPost
@ -154,7 +156,7 @@ $SnPublisherCopyWith<$Res> get publisher {
@JsonSerializable()
class _SnPost implements SnPost {
const _SnPost({required this.id, required this.title, required this.description, required this.language, required this.editedAt, required this.publishedAt, required this.visibility, required this.content, required this.type, required final Map<String, dynamic>? meta, required this.viewsUnique, required this.viewsTotal, required this.upvotes, required this.downvotes, required this.threadedPostId, required this.threadedPost, required this.repliedPostId, required this.repliedPost, required this.forwardedPostId, required this.forwardedPost, required final List<SnCloudFile> attachments, required this.publisher, final Map<String, int> reactionsCount = const {}, required final List<dynamic> reactions, required final List<dynamic> tags, required final List<dynamic> categories, required final List<dynamic> collections, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
const _SnPost({required this.id, required this.title, required this.description, required this.language, required this.editedAt, required this.publishedAt, required this.visibility, required this.content, required this.type, required final Map<String, dynamic>? meta, required this.viewsUnique, required this.viewsTotal, required this.upvotes, required this.downvotes, required this.repliesCount, required this.threadedPostId, required this.threadedPost, required this.repliedPostId, required this.repliedPost, required this.forwardedPostId, required this.forwardedPost, required final List<SnCloudFile> attachments, required this.publisher, final Map<String, int> reactionsCount = const {}, required final List<dynamic> reactions, required final List<dynamic> tags, required final List<dynamic> categories, required final List<dynamic> collections, required this.createdAt, required this.updatedAt, required this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@ -179,6 +181,7 @@ class _SnPost implements SnPost {
@override final int viewsTotal;
@override final int upvotes;
@override final int downvotes;
@override final int repliesCount;
@override final String? threadedPostId;
@override final SnPost? threadedPost;
@override final String? repliedPostId;
@ -231,6 +234,7 @@ class _SnPost implements SnPost {
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@override@JsonKey() final bool isTruncated;
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@ -245,16 +249,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@ -265,7 +269,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
@override @useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, bool isTruncated
});
@ -282,7 +286,7 @@ class __$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_SnPost(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@ -298,6 +302,7 @@ as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique :
as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable
as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable
as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable
@ -314,7 +319,8 @@ as List<dynamic>,collections: null == collections ? _self._collections : collect
as List<dynamic>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
as DateTime?,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable
as bool,
));
}
@ -370,7 +376,7 @@ $SnPublisherCopyWith<$Res> get publisher {
/// @nodoc
mixin _$SnPublisher {
String get id; int get type; String get name; String get nick; String get bio; SnCloudFile? get picture; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get realmId;
String get id; int get type; String get name; String get nick; String get bio; SnCloudFile? get picture; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get realmId; SnVerificationMark? get verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -383,16 +389,16 @@ $SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPu
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId);
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
@ -403,11 +409,11 @@ abstract mixin class $SnPublisherCopyWith<$Res> {
factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;$SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
@ -420,7 +426,7 @@ class _$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@ -435,7 +441,8 @@ as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore:
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
@ -474,6 +481,18 @@ $SnAccountCopyWith<$Res>? get account {
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
@ -482,7 +501,7 @@ $SnAccountCopyWith<$Res>? get account {
@JsonSerializable()
class _SnPublisher implements SnPublisher {
const _SnPublisher({required this.id, required this.type, required this.name, required this.nick, this.bio = '', required this.picture, required this.background, required this.account, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.realmId});
const _SnPublisher({required this.id, required this.type, required this.name, required this.nick, this.bio = '', required this.picture, required this.background, required this.account, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.realmId, required this.verification});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override final String id;
@ -498,6 +517,7 @@ class _SnPublisher implements SnPublisher {
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@override final String? realmId;
@override final SnVerificationMark? verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@ -512,16 +532,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId);
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
@ -532,11 +552,11 @@ abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith
factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;@override $SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
@ -549,7 +569,7 @@ class __$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@ -564,7 +584,8 @@ as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore:
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
@ -604,6 +625,18 @@ $SnAccountCopyWith<$Res>? get account {
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
@ -756,7 +789,7 @@ as int,
/// @nodoc
mixin _$SnSubscriptionStatus {
bool get isSubscribed; int get publisherId; String get publisherName;
bool get isSubscribed; String get publisherId; String get publisherName;
/// Create a copy of SnSubscriptionStatus
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -789,7 +822,7 @@ abstract mixin class $SnSubscriptionStatusCopyWith<$Res> {
factory $SnSubscriptionStatusCopyWith(SnSubscriptionStatus value, $Res Function(SnSubscriptionStatus) _then) = _$SnSubscriptionStatusCopyWithImpl;
@useResult
$Res call({
bool isSubscribed, int publisherId, String publisherName
bool isSubscribed, String publisherId, String publisherName
});
@ -810,7 +843,7 @@ class _$SnSubscriptionStatusCopyWithImpl<$Res>
return _then(_self.copyWith(
isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable
as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as int,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable
as String,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable
as String,
));
}
@ -826,7 +859,7 @@ class _SnSubscriptionStatus implements SnSubscriptionStatus {
factory _SnSubscriptionStatus.fromJson(Map<String, dynamic> json) => _$SnSubscriptionStatusFromJson(json);
@override final bool isSubscribed;
@override final int publisherId;
@override final String publisherId;
@override final String publisherName;
/// Create a copy of SnSubscriptionStatus
@ -862,7 +895,7 @@ abstract mixin class _$SnSubscriptionStatusCopyWith<$Res> implements $SnSubscrip
factory _$SnSubscriptionStatusCopyWith(_SnSubscriptionStatus value, $Res Function(_SnSubscriptionStatus) _then) = __$SnSubscriptionStatusCopyWithImpl;
@override @useResult
$Res call({
bool isSubscribed, int publisherId, String publisherName
bool isSubscribed, String publisherId, String publisherName
});
@ -883,7 +916,7 @@ class __$SnSubscriptionStatusCopyWithImpl<$Res>
return _then(_SnSubscriptionStatus(
isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable
as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as int,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable
as String,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable
as String,
));
}

View File

@ -24,6 +24,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
viewsTotal: (json['views_total'] as num).toInt(),
upvotes: (json['upvotes'] as num).toInt(),
downvotes: (json['downvotes'] as num).toInt(),
repliesCount: (json['replies_count'] as num).toInt(),
threadedPostId: json['threaded_post_id'] as String?,
threadedPost:
json['threaded_post'] == null
@ -59,6 +60,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
isTruncated: json['is_truncated'] as bool? ?? false,
);
Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
@ -76,6 +78,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'views_total': instance.viewsTotal,
'upvotes': instance.upvotes,
'downvotes': instance.downvotes,
'replies_count': instance.repliesCount,
'threaded_post_id': instance.threadedPostId,
'threaded_post': instance.threadedPost?.toJson(),
'replied_post_id': instance.repliedPostId,
@ -92,6 +95,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'is_truncated': instance.isTruncated,
};
_SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
@ -120,6 +124,12 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
? null
: DateTime.parse(json['deleted_at'] as String),
realmId: json['realm_id'] as String?,
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
@ -137,6 +147,7 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'realm_id': instance.realmId,
'verification': instance.verification?.toJson(),
};
_SnPublisherStats _$SnPublisherStatsFromJson(Map<String, dynamic> json) =>
@ -161,7 +172,7 @@ _SnSubscriptionStatus _$SnSubscriptionStatusFromJson(
Map<String, dynamic> json,
) => _SnSubscriptionStatus(
isSubscribed: json['is_subscribed'] as bool,
publisherId: (json['publisher_id'] as num).toInt(),
publisherId: json['publisher_id'] as String,
publisherName: json['publisher_name'] as String,
);

View File

@ -27,15 +27,23 @@ sealed class SnAccount with _$SnAccount {
sealed class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({
required String id,
required String? firstName,
required String? middleName,
required String? lastName,
@Default('') String firstName,
@Default('') String middleName,
@Default('') String lastName,
@Default('') String bio,
@Default('') String gender,
@Default('') String pronouns,
@Default('') String location,
@Default('') String timeZone,
DateTime? birthday,
DateTime? lastSeenAt,
SnAccountBadge? activeBadge,
required int experience,
required int level,
required double levelingProgress,
required SnCloudFile? picture,
required SnCloudFile? background,
required SnVerificationMark? verification,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
@ -78,6 +86,7 @@ sealed class SnAccountBadge with _$SnAccountBadge {
required String accountId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? activatedAt,
required DateTime? deletedAt,
}) = _SnAccountBadge;
@ -123,3 +132,16 @@ sealed class SnNotification with _$SnNotification {
factory SnNotification.fromJson(Map<String, dynamic> json) =>
_$SnNotificationFromJson(json);
}
@freezed
sealed class SnVerificationMark with _$SnVerificationMark {
const factory SnVerificationMark({
required int type,
required String? title,
required String? description,
required String? verifiedBy,
}) = _SnVerificationMark;
factory SnVerificationMark.fromJson(Map<String, dynamic> json) =>
_$SnVerificationMarkFromJson(json);
}

View File

@ -200,7 +200,7 @@ $SnAccountProfileCopyWith<$Res> get profile {
/// @nodoc
mixin _$SnAccountProfile {
String get id; String? get firstName; String? get middleName; String? get lastName; String get bio; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -213,16 +213,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,experience,level,levelingProgress,picture,background,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -233,11 +233,11 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult
$Res call({
String id, String? firstName, String? middleName, String? lastName, String bio, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;
$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
@ -250,19 +250,27 @@ class _$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,middleName: freezed == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String,middleName: null == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,gender: null == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable
as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast_nullable_to_non_nullable
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
@ -272,6 +280,18 @@ as DateTime?,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountBadgeCopyWith<$Res>? get activeBadge {
if (_self.activeBadge == null) {
return null;
}
return $SnAccountBadgeCopyWith<$Res>(_self.activeBadge!, (value) {
return _then(_self.copyWith(activeBadge: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
@ -292,6 +312,18 @@ $SnCloudFileCopyWith<$Res>? get background {
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
@ -300,19 +332,27 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, required this.firstName, required this.middleName, required this.lastName, this.bio = '', required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id;
@override final String? firstName;
@override final String? middleName;
@override final String? lastName;
@override@JsonKey() final String firstName;
@override@JsonKey() final String middleName;
@override@JsonKey() final String lastName;
@override@JsonKey() final String bio;
@override@JsonKey() final String gender;
@override@JsonKey() final String pronouns;
@override@JsonKey() final String location;
@override@JsonKey() final String timeZone;
@override final DateTime? birthday;
@override final DateTime? lastSeenAt;
@override final SnAccountBadge? activeBadge;
@override final int experience;
@override final int level;
@override final double levelingProgress;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnVerificationMark? verification;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@ -330,16 +370,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,experience,level,levelingProgress,picture,background,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -350,11 +390,11 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult
$Res call({
String id, String? firstName, String? middleName, String? lastName, String bio, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;
@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
@ -367,19 +407,27 @@ class __$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountProfile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,middleName: freezed == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String,middleName: null == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,gender: null == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable
as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast_nullable_to_non_nullable
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
@ -390,6 +438,18 @@ as DateTime?,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountBadgeCopyWith<$Res>? get activeBadge {
if (_self.activeBadge == null) {
return null;
}
return $SnAccountBadgeCopyWith<$Res>(_self.activeBadge!, (value) {
return _then(_self.copyWith(activeBadge: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
@ -410,6 +470,18 @@ $SnCloudFileCopyWith<$Res>? get background {
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
@ -583,7 +655,7 @@ as DateTime?,
/// @nodoc
mixin _$SnAccountBadge {
String get id; String get type; String? get label; String? get caption; Map<String, dynamic> get meta; DateTime? get expiredAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get type; String? get label; String? get caption; Map<String, dynamic> get meta; DateTime? get expiredAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get activatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountBadge
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -596,16 +668,16 @@ $SnAccountBadgeCopyWith<SnAccountBadge> get copyWith => _$SnAccountBadgeCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.activatedAt, activatedAt) || other.activatedAt == activatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(meta),expiredAt,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(meta),expiredAt,accountId,createdAt,updatedAt,activatedAt,deletedAt);
@override
String toString() {
return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, activatedAt: $activatedAt, deletedAt: $deletedAt)';
}
@ -616,7 +688,7 @@ abstract mixin class $SnAccountBadgeCopyWith<$Res> {
factory $SnAccountBadgeCopyWith(SnAccountBadge value, $Res Function(SnAccountBadge) _then) = _$SnAccountBadgeCopyWithImpl;
@useResult
$Res call({
String id, String type, String? label, String? caption, Map<String, dynamic> meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String type, String? label, String? caption, Map<String, dynamic> meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? activatedAt, DateTime? deletedAt
});
@ -633,7 +705,7 @@ class _$SnAccountBadgeCopyWithImpl<$Res>
/// Create a copy of SnAccountBadge
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? activatedAt = freezed,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@ -644,7 +716,8 @@ as Map<String, dynamic>,expiredAt: freezed == expiredAt ? _self.expiredAt : expi
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime,activatedAt: freezed == activatedAt ? _self.activatedAt : activatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
@ -656,7 +729,7 @@ as DateTime?,
@JsonSerializable()
class _SnAccountBadge implements SnAccountBadge {
const _SnAccountBadge({required this.id, required this.type, required this.label, required this.caption, required final Map<String, dynamic> meta, required this.expiredAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta;
const _SnAccountBadge({required this.id, required this.type, required this.label, required this.caption, required final Map<String, dynamic> meta, required this.expiredAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.activatedAt, required this.deletedAt}): _meta = meta;
factory _SnAccountBadge.fromJson(Map<String, dynamic> json) => _$SnAccountBadgeFromJson(json);
@override final String id;
@ -674,6 +747,7 @@ class _SnAccountBadge implements SnAccountBadge {
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? activatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAccountBadge
@ -689,16 +763,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.activatedAt, activatedAt) || other.activatedAt == activatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(_meta),expiredAt,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(_meta),expiredAt,accountId,createdAt,updatedAt,activatedAt,deletedAt);
@override
String toString() {
return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, activatedAt: $activatedAt, deletedAt: $deletedAt)';
}
@ -709,7 +783,7 @@ abstract mixin class _$SnAccountBadgeCopyWith<$Res> implements $SnAccountBadgeCo
factory _$SnAccountBadgeCopyWith(_SnAccountBadge value, $Res Function(_SnAccountBadge) _then) = __$SnAccountBadgeCopyWithImpl;
@override @useResult
$Res call({
String id, String type, String? label, String? caption, Map<String, dynamic> meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String type, String? label, String? caption, Map<String, dynamic> meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? activatedAt, DateTime? deletedAt
});
@ -726,7 +800,7 @@ class __$SnAccountBadgeCopyWithImpl<$Res>
/// Create a copy of SnAccountBadge
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? activatedAt = freezed,Object? deletedAt = freezed,}) {
return _then(_SnAccountBadge(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@ -737,7 +811,8 @@ as Map<String, dynamic>,expiredAt: freezed == expiredAt ? _self.expiredAt : expi
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime,activatedAt: freezed == activatedAt ? _self.activatedAt : activatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
@ -1072,6 +1147,148 @@ as String,
}
}
/// @nodoc
mixin _$SnVerificationMark {
int get type; String? get title; String? get description; String? get verifiedBy;
/// Create a copy of SnVerificationMark
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<SnVerificationMark> get copyWith => _$SnVerificationMarkCopyWithImpl<SnVerificationMark>(this as SnVerificationMark, _$identity);
/// Serializes this SnVerificationMark to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnVerificationMark&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.verifiedBy, verifiedBy) || other.verifiedBy == verifiedBy));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,title,description,verifiedBy);
@override
String toString() {
return 'SnVerificationMark(type: $type, title: $title, description: $description, verifiedBy: $verifiedBy)';
}
}
/// @nodoc
abstract mixin class $SnVerificationMarkCopyWith<$Res> {
factory $SnVerificationMarkCopyWith(SnVerificationMark value, $Res Function(SnVerificationMark) _then) = _$SnVerificationMarkCopyWithImpl;
@useResult
$Res call({
int type, String? title, String? description, String? verifiedBy
});
}
/// @nodoc
class _$SnVerificationMarkCopyWithImpl<$Res>
implements $SnVerificationMarkCopyWith<$Res> {
_$SnVerificationMarkCopyWithImpl(this._self, this._then);
final SnVerificationMark _self;
final $Res Function(SnVerificationMark) _then;
/// Create a copy of SnVerificationMark
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? title = freezed,Object? description = freezed,Object? verifiedBy = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,verifiedBy: freezed == verifiedBy ? _self.verifiedBy : verifiedBy // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnVerificationMark implements SnVerificationMark {
const _SnVerificationMark({required this.type, required this.title, required this.description, required this.verifiedBy});
factory _SnVerificationMark.fromJson(Map<String, dynamic> json) => _$SnVerificationMarkFromJson(json);
@override final int type;
@override final String? title;
@override final String? description;
@override final String? verifiedBy;
/// Create a copy of SnVerificationMark
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnVerificationMarkCopyWith<_SnVerificationMark> get copyWith => __$SnVerificationMarkCopyWithImpl<_SnVerificationMark>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnVerificationMarkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnVerificationMark&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.verifiedBy, verifiedBy) || other.verifiedBy == verifiedBy));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,title,description,verifiedBy);
@override
String toString() {
return 'SnVerificationMark(type: $type, title: $title, description: $description, verifiedBy: $verifiedBy)';
}
}
/// @nodoc
abstract mixin class _$SnVerificationMarkCopyWith<$Res> implements $SnVerificationMarkCopyWith<$Res> {
factory _$SnVerificationMarkCopyWith(_SnVerificationMark value, $Res Function(_SnVerificationMark) _then) = __$SnVerificationMarkCopyWithImpl;
@override @useResult
$Res call({
int type, String? title, String? description, String? verifiedBy
});
}
/// @nodoc
class __$SnVerificationMarkCopyWithImpl<$Res>
implements _$SnVerificationMarkCopyWith<$Res> {
__$SnVerificationMarkCopyWithImpl(this._self, this._then);
final _SnVerificationMark _self;
final $Res Function(_SnVerificationMark) _then;
/// Create a copy of SnVerificationMark
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? title = freezed,Object? description = freezed,Object? verifiedBy = freezed,}) {
return _then(_SnVerificationMark(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,verifiedBy: freezed == verifiedBy ? _self.verifiedBy : verifiedBy // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on

View File

@ -43,10 +43,28 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile(
id: json['id'] as String,
firstName: json['first_name'] as String?,
middleName: json['middle_name'] as String?,
lastName: json['last_name'] as String?,
firstName: json['first_name'] as String? ?? '',
middleName: json['middle_name'] as String? ?? '',
lastName: json['last_name'] as String? ?? '',
bio: json['bio'] as String? ?? '',
gender: json['gender'] as String? ?? '',
pronouns: json['pronouns'] as String? ?? '',
location: json['location'] as String? ?? '',
timeZone: json['time_zone'] as String? ?? '',
birthday:
json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
lastSeenAt:
json['last_seen_at'] == null
? null
: DateTime.parse(json['last_seen_at'] as String),
activeBadge:
json['active_badge'] == null
? null
: SnAccountBadge.fromJson(
json['active_badge'] as Map<String, dynamic>,
),
experience: (json['experience'] as num).toInt(),
level: (json['level'] as num).toInt(),
levelingProgress: (json['leveling_progress'] as num).toDouble(),
@ -60,6 +78,12 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
: SnCloudFile.fromJson(
json['background'] as Map<String, dynamic>,
),
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
@ -75,11 +99,19 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'middle_name': instance.middleName,
'last_name': instance.lastName,
'bio': instance.bio,
'gender': instance.gender,
'pronouns': instance.pronouns,
'location': instance.location,
'time_zone': instance.timeZone,
'birthday': instance.birthday?.toIso8601String(),
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'active_badge': instance.activeBadge?.toJson(),
'experience': instance.experience,
'level': instance.level,
'leveling_progress': instance.levelingProgress,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'verification': instance.verification?.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
@ -137,6 +169,10 @@ _SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) =>
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
activatedAt:
json['activated_at'] == null
? null
: DateTime.parse(json['activated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
@ -154,6 +190,7 @@ Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'activated_at': instance.activatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
@ -226,3 +263,19 @@ Map<String, dynamic> _$SnNotificationToJson(_SnNotification instance) =>
'viewed_at': instance.viewedAt?.toIso8601String(),
'account_id': instance.accountId,
};
_SnVerificationMark _$SnVerificationMarkFromJson(Map<String, dynamic> json) =>
_SnVerificationMark(
type: (json['type'] as num).toInt(),
title: json['title'] as String?,
description: json['description'] as String?,
verifiedBy: json['verified_by'] as String?,
);
Map<String, dynamic> _$SnVerificationMarkToJson(_SnVerificationMark instance) =>
<String, dynamic>{
'type': instance.type,
'title': instance.title,
'description': instance.description,
'verified_by': instance.verifiedBy,
};

View File

@ -56,3 +56,32 @@ sealed class SnTransaction with _$SnTransaction {
factory SnTransaction.fromJson(Map<String, dynamic> json) =>
_$SnTransactionFromJson(json);
}
@freezed
sealed class SnWalletSubscription with _$SnWalletSubscription {
const factory SnWalletSubscription({
required String id,
required DateTime begunAt,
required DateTime endedAt,
required String identifier,
required bool isActive,
required bool isFreeTrial,
required int status,
required String paymentMethod,
required Map<String, dynamic> paymentDetails,
required double basePrice,
required String? couponId,
required dynamic coupon,
required DateTime renewalAt,
required String accountId,
required SnAccount? account,
required bool isAvailable,
required double finalPrice,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnWalletSubscription;
factory SnWalletSubscription.fromJson(Map<String, dynamic> json) =>
_$SnWalletSubscriptionFromJson(json);
}

View File

@ -558,4 +558,224 @@ $SnWalletCopyWith<$Res>? get payeeWallet {
}
}
/// @nodoc
mixin _$SnWalletSubscription {
String get id; DateTime get begunAt; DateTime get endedAt; String get identifier; bool get isActive; bool get isFreeTrial; int get status; String get paymentMethod; Map<String, dynamic> get paymentDetails; double get basePrice; String? get couponId; dynamic get coupon; DateTime get renewalAt; String get accountId; SnAccount? get account; bool get isAvailable; double get finalPrice; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnWalletSubscription
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWalletSubscriptionCopyWith<SnWalletSubscription> get copyWith => _$SnWalletSubscriptionCopyWithImpl<SnWalletSubscription>(this as SnWalletSubscription, _$identity);
/// Serializes this SnWalletSubscription to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWalletSubscription&&(identical(other.id, id) || other.id == id)&&(identical(other.begunAt, begunAt) || other.begunAt == begunAt)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.identifier, identifier) || other.identifier == identifier)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.isFreeTrial, isFreeTrial) || other.isFreeTrial == isFreeTrial)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentMethod, paymentMethod) || other.paymentMethod == paymentMethod)&&const DeepCollectionEquality().equals(other.paymentDetails, paymentDetails)&&(identical(other.basePrice, basePrice) || other.basePrice == basePrice)&&(identical(other.couponId, couponId) || other.couponId == couponId)&&const DeepCollectionEquality().equals(other.coupon, coupon)&&(identical(other.renewalAt, renewalAt) || other.renewalAt == renewalAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.isAvailable, isAvailable) || other.isAvailable == isAvailable)&&(identical(other.finalPrice, finalPrice) || other.finalPrice == finalPrice)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,begunAt,endedAt,identifier,isActive,isFreeTrial,status,paymentMethod,const DeepCollectionEquality().hash(paymentDetails),basePrice,couponId,const DeepCollectionEquality().hash(coupon),renewalAt,accountId,account,isAvailable,finalPrice,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnWalletSubscription(id: $id, begunAt: $begunAt, endedAt: $endedAt, identifier: $identifier, isActive: $isActive, isFreeTrial: $isFreeTrial, status: $status, paymentMethod: $paymentMethod, paymentDetails: $paymentDetails, basePrice: $basePrice, couponId: $couponId, coupon: $coupon, renewalAt: $renewalAt, accountId: $accountId, account: $account, isAvailable: $isAvailable, finalPrice: $finalPrice, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnWalletSubscriptionCopyWith<$Res> {
factory $SnWalletSubscriptionCopyWith(SnWalletSubscription value, $Res Function(SnWalletSubscription) _then) = _$SnWalletSubscriptionCopyWithImpl;
@useResult
$Res call({
String id, DateTime begunAt, DateTime endedAt, String identifier, bool isActive, bool isFreeTrial, int status, String paymentMethod, Map<String, dynamic> paymentDetails, double basePrice, String? couponId, dynamic coupon, DateTime renewalAt, String accountId, SnAccount? account, bool isAvailable, double finalPrice, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class _$SnWalletSubscriptionCopyWithImpl<$Res>
implements $SnWalletSubscriptionCopyWith<$Res> {
_$SnWalletSubscriptionCopyWithImpl(this._self, this._then);
final SnWalletSubscription _self;
final $Res Function(SnWalletSubscription) _then;
/// Create a copy of SnWalletSubscription
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? begunAt = null,Object? endedAt = null,Object? identifier = null,Object? isActive = null,Object? isFreeTrial = null,Object? status = null,Object? paymentMethod = null,Object? paymentDetails = null,Object? basePrice = null,Object? couponId = freezed,Object? coupon = freezed,Object? renewalAt = null,Object? accountId = null,Object? account = freezed,Object? isAvailable = null,Object? finalPrice = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,begunAt: null == begunAt ? _self.begunAt : begunAt // ignore: cast_nullable_to_non_nullable
as DateTime,endedAt: null == endedAt ? _self.endedAt : endedAt // ignore: cast_nullable_to_non_nullable
as DateTime,identifier: null == identifier ? _self.identifier : identifier // ignore: cast_nullable_to_non_nullable
as String,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable
as bool,isFreeTrial: null == isFreeTrial ? _self.isFreeTrial : isFreeTrial // ignore: cast_nullable_to_non_nullable
as bool,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,paymentMethod: null == paymentMethod ? _self.paymentMethod : paymentMethod // ignore: cast_nullable_to_non_nullable
as String,paymentDetails: null == paymentDetails ? _self.paymentDetails : paymentDetails // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,basePrice: null == basePrice ? _self.basePrice : basePrice // ignore: cast_nullable_to_non_nullable
as double,couponId: freezed == couponId ? _self.couponId : couponId // ignore: cast_nullable_to_non_nullable
as String?,coupon: freezed == coupon ? _self.coupon : coupon // ignore: cast_nullable_to_non_nullable
as dynamic,renewalAt: null == renewalAt ? _self.renewalAt : renewalAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,isAvailable: null == isAvailable ? _self.isAvailable : isAvailable // ignore: cast_nullable_to_non_nullable
as bool,finalPrice: null == finalPrice ? _self.finalPrice : finalPrice // ignore: cast_nullable_to_non_nullable
as double,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWalletSubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnWalletSubscription implements SnWalletSubscription {
const _SnWalletSubscription({required this.id, required this.begunAt, required this.endedAt, required this.identifier, required this.isActive, required this.isFreeTrial, required this.status, required this.paymentMethod, required final Map<String, dynamic> paymentDetails, required this.basePrice, required this.couponId, required this.coupon, required this.renewalAt, required this.accountId, required this.account, required this.isAvailable, required this.finalPrice, required this.createdAt, required this.updatedAt, required this.deletedAt}): _paymentDetails = paymentDetails;
factory _SnWalletSubscription.fromJson(Map<String, dynamic> json) => _$SnWalletSubscriptionFromJson(json);
@override final String id;
@override final DateTime begunAt;
@override final DateTime endedAt;
@override final String identifier;
@override final bool isActive;
@override final bool isFreeTrial;
@override final int status;
@override final String paymentMethod;
final Map<String, dynamic> _paymentDetails;
@override Map<String, dynamic> get paymentDetails {
if (_paymentDetails is EqualUnmodifiableMapView) return _paymentDetails;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_paymentDetails);
}
@override final double basePrice;
@override final String? couponId;
@override final dynamic coupon;
@override final DateTime renewalAt;
@override final String accountId;
@override final SnAccount? account;
@override final bool isAvailable;
@override final double finalPrice;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnWalletSubscription
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWalletSubscriptionCopyWith<_SnWalletSubscription> get copyWith => __$SnWalletSubscriptionCopyWithImpl<_SnWalletSubscription>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWalletSubscriptionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWalletSubscription&&(identical(other.id, id) || other.id == id)&&(identical(other.begunAt, begunAt) || other.begunAt == begunAt)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.identifier, identifier) || other.identifier == identifier)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.isFreeTrial, isFreeTrial) || other.isFreeTrial == isFreeTrial)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentMethod, paymentMethod) || other.paymentMethod == paymentMethod)&&const DeepCollectionEquality().equals(other._paymentDetails, _paymentDetails)&&(identical(other.basePrice, basePrice) || other.basePrice == basePrice)&&(identical(other.couponId, couponId) || other.couponId == couponId)&&const DeepCollectionEquality().equals(other.coupon, coupon)&&(identical(other.renewalAt, renewalAt) || other.renewalAt == renewalAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.isAvailable, isAvailable) || other.isAvailable == isAvailable)&&(identical(other.finalPrice, finalPrice) || other.finalPrice == finalPrice)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,begunAt,endedAt,identifier,isActive,isFreeTrial,status,paymentMethod,const DeepCollectionEquality().hash(_paymentDetails),basePrice,couponId,const DeepCollectionEquality().hash(coupon),renewalAt,accountId,account,isAvailable,finalPrice,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnWalletSubscription(id: $id, begunAt: $begunAt, endedAt: $endedAt, identifier: $identifier, isActive: $isActive, isFreeTrial: $isFreeTrial, status: $status, paymentMethod: $paymentMethod, paymentDetails: $paymentDetails, basePrice: $basePrice, couponId: $couponId, coupon: $coupon, renewalAt: $renewalAt, accountId: $accountId, account: $account, isAvailable: $isAvailable, finalPrice: $finalPrice, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnWalletSubscriptionCopyWith<$Res> implements $SnWalletSubscriptionCopyWith<$Res> {
factory _$SnWalletSubscriptionCopyWith(_SnWalletSubscription value, $Res Function(_SnWalletSubscription) _then) = __$SnWalletSubscriptionCopyWithImpl;
@override @useResult
$Res call({
String id, DateTime begunAt, DateTime endedAt, String identifier, bool isActive, bool isFreeTrial, int status, String paymentMethod, Map<String, dynamic> paymentDetails, double basePrice, String? couponId, dynamic coupon, DateTime renewalAt, String accountId, SnAccount? account, bool isAvailable, double finalPrice, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class __$SnWalletSubscriptionCopyWithImpl<$Res>
implements _$SnWalletSubscriptionCopyWith<$Res> {
__$SnWalletSubscriptionCopyWithImpl(this._self, this._then);
final _SnWalletSubscription _self;
final $Res Function(_SnWalletSubscription) _then;
/// Create a copy of SnWalletSubscription
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? begunAt = null,Object? endedAt = null,Object? identifier = null,Object? isActive = null,Object? isFreeTrial = null,Object? status = null,Object? paymentMethod = null,Object? paymentDetails = null,Object? basePrice = null,Object? couponId = freezed,Object? coupon = freezed,Object? renewalAt = null,Object? accountId = null,Object? account = freezed,Object? isAvailable = null,Object? finalPrice = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnWalletSubscription(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,begunAt: null == begunAt ? _self.begunAt : begunAt // ignore: cast_nullable_to_non_nullable
as DateTime,endedAt: null == endedAt ? _self.endedAt : endedAt // ignore: cast_nullable_to_non_nullable
as DateTime,identifier: null == identifier ? _self.identifier : identifier // ignore: cast_nullable_to_non_nullable
as String,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable
as bool,isFreeTrial: null == isFreeTrial ? _self.isFreeTrial : isFreeTrial // ignore: cast_nullable_to_non_nullable
as bool,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,paymentMethod: null == paymentMethod ? _self.paymentMethod : paymentMethod // ignore: cast_nullable_to_non_nullable
as String,paymentDetails: null == paymentDetails ? _self._paymentDetails : paymentDetails // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,basePrice: null == basePrice ? _self.basePrice : basePrice // ignore: cast_nullable_to_non_nullable
as double,couponId: freezed == couponId ? _self.couponId : couponId // ignore: cast_nullable_to_non_nullable
as String?,coupon: freezed == coupon ? _self.coupon : coupon // ignore: cast_nullable_to_non_nullable
as dynamic,renewalAt: null == renewalAt ? _self.renewalAt : renewalAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,isAvailable: null == isAvailable ? _self.isAvailable : isAvailable // ignore: cast_nullable_to_non_nullable
as bool,finalPrice: null == finalPrice ? _self.finalPrice : finalPrice // ignore: cast_nullable_to_non_nullable
as double,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWalletSubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
// dart format on

View File

@ -100,3 +100,59 @@ Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnWalletSubscription _$SnWalletSubscriptionFromJson(
Map<String, dynamic> json,
) => _SnWalletSubscription(
id: json['id'] as String,
begunAt: DateTime.parse(json['begun_at'] as String),
endedAt: DateTime.parse(json['ended_at'] as String),
identifier: json['identifier'] as String,
isActive: json['is_active'] as bool,
isFreeTrial: json['is_free_trial'] as bool,
status: (json['status'] as num).toInt(),
paymentMethod: json['payment_method'] as String,
paymentDetails: json['payment_details'] as Map<String, dynamic>,
basePrice: (json['base_price'] as num).toDouble(),
couponId: json['coupon_id'] as String?,
coupon: json['coupon'],
renewalAt: DateTime.parse(json['renewal_at'] as String),
accountId: json['account_id'] as String,
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
isAvailable: json['is_available'] as bool,
finalPrice: (json['final_price'] as num).toDouble(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnWalletSubscriptionToJson(
_SnWalletSubscription instance,
) => <String, dynamic>{
'id': instance.id,
'begun_at': instance.begunAt.toIso8601String(),
'ended_at': instance.endedAt.toIso8601String(),
'identifier': instance.identifier,
'is_active': instance.isActive,
'is_free_trial': instance.isFreeTrial,
'status': instance.status,
'payment_method': instance.paymentMethod,
'payment_details': instance.paymentDetails,
'base_price': instance.basePrice,
'coupon_id': instance.couponId,
'coupon': instance.coupon,
'renewal_at': instance.renewalAt.toIso8601String(),
'account_id': instance.accountId,
'account': instance.account?.toJson(),
'is_available': instance.isAvailable,
'final_price': instance.finalPrice,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -66,6 +66,8 @@ class CallNotifier extends _$CallNotifier {
Timer? _durationTimer;
Room? get room => _room;
@override
CallState build() {
// Subscribe to websocket updates
@ -230,6 +232,9 @@ class CallNotifier extends _$CallNotifier {
String? get roomId => _roomId;
Future<void> joinRoom(String roomId) async {
if (_roomId == roomId && _room != null) {
return;
}
_roomId = roomId;
if (_room != null) {
await _room!.disconnect();

View File

@ -6,7 +6,7 @@ part of 'call.dart';
// RiverpodGenerator
// **************************************************************************
String _$callNotifierHash() => r'2082a572b5cfb4bf929dc1ed492c52cd2735452e';
String _$callNotifierHash() => r'47eaba43aa2af1a107725998f4a34af2c94fbc55';
/// See also [CallNotifier].
@ProviderFor(CallNotifier)

View File

@ -0,0 +1,56 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/pods/network.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'event_calendar.g.dart';
/// Query parameters for fetching event calendar data
class EventCalendarQuery {
/// Username to fetch calendar for, null means current user ('me')
final String? uname;
/// Year to fetch calendar for
final int year;
/// Month to fetch calendar for
final int month;
const EventCalendarQuery({
required this.uname,
required this.year,
required this.month,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is EventCalendarQuery &&
runtimeType == other.runtimeType &&
uname == other.uname &&
year == other.year &&
month == other.month;
@override
int get hashCode => uname.hashCode ^ year.hashCode ^ month.hashCode;
}
/// Provider for fetching event calendar data
/// This can be used anywhere in the app where calendar data is needed
@riverpod
Future<List<SnEventCalendarEntry>> eventCalendar(
Ref ref,
EventCalendarQuery query,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/accounts/${query.uname ?? 'me'}/calendar',
queryParameters: {
'year': query.year,
'month': query.month,
},
);
return resp.data
.map((e) => SnEventCalendarEntry.fromJson(e))
.cast<SnEventCalendarEntry>()
.toList();
}

View File

@ -6,8 +6,7 @@ part of 'event_calendar.dart';
// RiverpodGenerator
// **************************************************************************
String _$accountEventCalendarHash() =>
r'57405caaf53a83d121b6bb4b70540134fb581525';
String _$eventCalendarHash() => r'6f2454404fa8660b96334d654490e1a40ee53e10';
/// Copied from Dart SDK
class _SystemHash {
@ -30,24 +29,36 @@ class _SystemHash {
}
}
/// See also [accountEventCalendar].
@ProviderFor(accountEventCalendar)
const accountEventCalendarProvider = AccountEventCalendarFamily();
/// Provider for fetching event calendar data
/// This can be used anywhere in the app where calendar data is needed
///
/// Copied from [eventCalendar].
@ProviderFor(eventCalendar)
const eventCalendarProvider = EventCalendarFamily();
/// See also [accountEventCalendar].
class AccountEventCalendarFamily
/// Provider for fetching event calendar data
/// This can be used anywhere in the app where calendar data is needed
///
/// Copied from [eventCalendar].
class EventCalendarFamily
extends Family<AsyncValue<List<SnEventCalendarEntry>>> {
/// See also [accountEventCalendar].
const AccountEventCalendarFamily();
/// Provider for fetching event calendar data
/// This can be used anywhere in the app where calendar data is needed
///
/// Copied from [eventCalendar].
const EventCalendarFamily();
/// See also [accountEventCalendar].
AccountEventCalendarProvider call(EventCalendarQuery query) {
return AccountEventCalendarProvider(query);
/// Provider for fetching event calendar data
/// This can be used anywhere in the app where calendar data is needed
///
/// Copied from [eventCalendar].
EventCalendarProvider call(EventCalendarQuery query) {
return EventCalendarProvider(query);
}
@override
AccountEventCalendarProvider getProviderOverride(
covariant AccountEventCalendarProvider provider,
EventCalendarProvider getProviderOverride(
covariant EventCalendarProvider provider,
) {
return call(provider.query);
}
@ -64,29 +75,35 @@ class AccountEventCalendarFamily
_allTransitiveDependencies;
@override
String? get name => r'accountEventCalendarProvider';
String? get name => r'eventCalendarProvider';
}
/// See also [accountEventCalendar].
class AccountEventCalendarProvider
/// Provider for fetching event calendar data
/// This can be used anywhere in the app where calendar data is needed
///
/// Copied from [eventCalendar].
class EventCalendarProvider
extends AutoDisposeFutureProvider<List<SnEventCalendarEntry>> {
/// See also [accountEventCalendar].
AccountEventCalendarProvider(EventCalendarQuery query)
/// Provider for fetching event calendar data
/// This can be used anywhere in the app where calendar data is needed
///
/// Copied from [eventCalendar].
EventCalendarProvider(EventCalendarQuery query)
: this._internal(
(ref) => accountEventCalendar(ref as AccountEventCalendarRef, query),
from: accountEventCalendarProvider,
name: r'accountEventCalendarProvider',
(ref) => eventCalendar(ref as EventCalendarRef, query),
from: eventCalendarProvider,
name: r'eventCalendarProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountEventCalendarHash,
dependencies: AccountEventCalendarFamily._dependencies,
: _$eventCalendarHash,
dependencies: EventCalendarFamily._dependencies,
allTransitiveDependencies:
AccountEventCalendarFamily._allTransitiveDependencies,
EventCalendarFamily._allTransitiveDependencies,
query: query,
);
AccountEventCalendarProvider._internal(
EventCalendarProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
@ -100,15 +117,13 @@ class AccountEventCalendarProvider
@override
Override overrideWith(
FutureOr<List<SnEventCalendarEntry>> Function(
AccountEventCalendarRef provider,
)
FutureOr<List<SnEventCalendarEntry>> Function(EventCalendarRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: AccountEventCalendarProvider._internal(
(ref) => create(ref as AccountEventCalendarRef),
override: EventCalendarProvider._internal(
(ref) => create(ref as EventCalendarRef),
from: from,
name: null,
dependencies: null,
@ -121,12 +136,12 @@ class AccountEventCalendarProvider
@override
AutoDisposeFutureProviderElement<List<SnEventCalendarEntry>> createElement() {
return _AccountEventCalendarProviderElement(this);
return _EventCalendarProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountEventCalendarProvider && other.query == query;
return other is EventCalendarProvider && other.query == query;
}
@override
@ -140,20 +155,19 @@ class AccountEventCalendarProvider
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountEventCalendarRef
mixin EventCalendarRef
on AutoDisposeFutureProviderRef<List<SnEventCalendarEntry>> {
/// The parameter `query` of this provider.
EventCalendarQuery get query;
}
class _AccountEventCalendarProviderElement
class _EventCalendarProviderElement
extends AutoDisposeFutureProviderElement<List<SnEventCalendarEntry>>
with AccountEventCalendarRef {
_AccountEventCalendarProviderElement(super.provider);
with EventCalendarRef {
_EventCalendarProviderElement(super.provider);
@override
EventCalendarQuery get query =>
(origin as AccountEventCalendarProvider).query;
EventCalendarQuery get query => (origin as EventCalendarProvider).query;
}
// ignore_for_file: type=lint

View File

@ -16,27 +16,42 @@ import 'config.dart';
final imagePickerProvider = Provider((ref) => ImagePicker());
final userAgentProvider = FutureProvider<String>((ref) async {
// Helper function to sanitize strings for HTTP headers
String sanitizeForHeader(String input) {
// Remove or replace characters that are not allowed in HTTP headers
// Keep only ASCII printable characters (32-126) and replace others with underscore
return input.runes.map((rune) {
if (rune >= 32 && rune <= 126) {
return String.fromCharCode(rune);
} else {
return '_';
}
}).join();
}
final String platformInfo;
if (kIsWeb) {
final deviceInfo = await DeviceInfoPlugin().webBrowserInfo;
platformInfo = 'Web; ${deviceInfo.vendor}';
platformInfo = 'Web; ${sanitizeForHeader(deviceInfo.vendor ?? 'Unknown')}';
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
platformInfo =
'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
'Android; ${sanitizeForHeader(deviceInfo.brand)} ${sanitizeForHeader(deviceInfo.model)}; ${sanitizeForHeader(deviceInfo.id)}';
} else if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
platformInfo =
'iOS; ${sanitizeForHeader(deviceInfo.model)}; ${sanitizeForHeader(deviceInfo.name)}';
} else if (Platform.isMacOS) {
final deviceInfo = await DeviceInfoPlugin().macOsInfo;
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
platformInfo =
'MacOS; ${sanitizeForHeader(deviceInfo.model)}; ${sanitizeForHeader(deviceInfo.hostName)}';
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
platformInfo =
'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
'Windows NT; ${sanitizeForHeader(deviceInfo.productName)}; ${sanitizeForHeader(deviceInfo.computerName)}';
} else if (Platform.isLinux) {
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
platformInfo = 'Linux; ${deviceInfo.prettyName}';
platformInfo = 'Linux; ${sanitizeForHeader(deviceInfo.prettyName)}';
} else {
platformInfo = 'Unknown';
}

View File

@ -33,6 +33,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey);
_ref.invalidate(userInfoProvider);
_ref.invalidate(tokenProvider);
}
}

View File

@ -9,48 +9,59 @@ class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(
page: ExploreShellRoute.page,
page: TabsRoute.page,
path: '/',
children: [
AutoRoute(page: ExploreRoute.page, path: ''),
AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'),
AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'),
AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'),
AutoRoute(page: PublisherProfileRoute.page, path: 'publishers/:name'),
],
),
AutoRoute(
page: AccountShellRoute.page,
path: '/account',
children: [
AutoRoute(page: AccountRoute.page, path: ''),
AutoRoute(page: NotificationRoute.page, path: 'notifications'),
AutoRoute(page: WalletRoute.page, path: 'wallet'),
AutoRoute(page: RelationshipRoute.page, path: 'relationships'),
AutoRoute(page: AccountProfileRoute.page, path: ':name'),
AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'),
AutoRoute(page: AccountSettingsRoute.page, path: 'settings'),
AutoRoute(
page: ExploreShellRoute.page,
path: '',
children: [
AutoRoute(page: ExploreRoute.page, path: ''),
AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'),
AutoRoute(
page: PublisherProfileRoute.page,
path: 'publishers/:name',
),
],
),
AutoRoute(
page: AccountShellRoute.page,
path: 'account',
children: [
AutoRoute(page: AccountRoute.page, path: ''),
AutoRoute(page: NotificationRoute.page, path: 'notifications'),
AutoRoute(page: WalletRoute.page, path: 'wallet'),
AutoRoute(page: RelationshipRoute.page, path: 'relationships'),
AutoRoute(page: AccountProfileRoute.page, path: ':name'),
AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'),
AutoRoute(page: LevelingRoute.page, path: 'me/leveling'),
AutoRoute(page: AccountSettingsRoute.page, path: 'settings'),
],
),
AutoRoute(page: RealmListRoute.page, path: 'realms'),
AutoRoute(
page: ChatShellRoute.page,
path: 'chat',
children: [
AutoRoute(page: ChatListRoute.page, path: ''),
AutoRoute(page: ChatRoomRoute.page, path: ':id'),
AutoRoute(page: NewChatRoute.page, path: 'new'),
AutoRoute(page: EditChatRoute.page, path: ':id/edit'),
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'),
],
),
],
),
AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'),
AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'),
AutoRoute(page: CallRoute.page, path: '/chat/:id/call'),
AutoRoute(page: EventCalanderRoute.page, path: '/account/:name/calendar'),
AutoRoute(page: RealmListRoute.page, path: '/realms'),
AutoRoute(
page: ChatShellRoute.page,
path: '/chat',
children: [
AutoRoute(page: ChatListRoute.page, path: ''),
AutoRoute(page: ChatRoomRoute.page, path: ':id'),
AutoRoute(page: CallRoute.page, path: ':id/call'),
AutoRoute(page: NewChatRoute.page, path: 'new'),
AutoRoute(page: EditChatRoute.page, path: ':id/edit'),
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'),
],
),
AutoRoute(
page: CreatorHubShellRoute.page,
path: '/creators',
children: [
AutoRoute(page: CreatorHubRoute.page, path: ''),
AutoRoute(page: CreatorPostListRoute.page, path: ':name/posts'),
AutoRoute(page: StickersRoute.page, path: ':name/stickers'),
AutoRoute(page: NewStickerPacksRoute.page, path: ':name/stickers/new'),
AutoRoute(

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/app_scaffold.dart';
@ -66,8 +67,9 @@ class AccountScreen extends HookConsumerWidget {
return AppScaffold(
noBackground: isWide,
appBar: AppBar(title: const Text('account').tr()),
appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0),
body: SingleChildScrollView(
padding: getTabbedPadding(context),
child: Column(
children: <Widget>[
Card(
@ -83,7 +85,7 @@ class AccountScreen extends HookConsumerWidget {
child: AspectRatio(
aspectRatio: 16 / 7,
child: CloudImageWidget(
fileId: user.value!.profile.background!.id,
file: user.value?.profile.background,
fit: BoxFit.cover,
),
),
@ -94,7 +96,7 @@ class AccountScreen extends HookConsumerWidget {
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: user.value?.profile.picture?.id,
file: user.value?.profile.picture,
radius: 24,
),
onTap: () {
@ -112,7 +114,13 @@ class AccountScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(user.value!.nick).bold().fontSize(16),
AccountName(
account: user.value!,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text('@${user.value!.name}'),
],
),
@ -132,11 +140,16 @@ class AccountScreen extends HookConsumerWidget {
],
),
).padding(horizontal: 8),
LevelingProgressCard(
level: user.value!.profile.level,
experience: user.value!.profile.experience,
progress: user.value!.profile.levelingProgress,
).padding(horizontal: 8),
GestureDetector(
child: LevelingProgressCard(
level: user.value!.profile.level,
experience: user.value!.profile.experience,
progress: user.value!.profile.levelingProgress,
),
onTap: () {
context.router.push(LevelingRoute());
},
).padding(horizontal: 12),
Row(
children: [
Expanded(
@ -214,16 +227,6 @@ class AccountScreen extends HookConsumerWidget {
context.router.push(RelationshipRoute());
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('updateYourProfile').tr(),
onTap: () {
context.router.push(UpdateProfileRoute());
},
),
const Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
@ -235,6 +238,16 @@ class AccountScreen extends HookConsumerWidget {
context.router.push(SettingsRoute());
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.person_edit),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('updateYourProfile').tr(),
onTap: () {
context.router.push(UpdateProfileRoute());
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.manage_accounts),

View File

@ -0,0 +1,120 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/event_calendar.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/widgets/account/account_nameplate.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/account/event_calendar.dart';
import 'package:island/widgets/account/fortune_graph.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class EventCalanderScreen extends HookConsumerWidget {
final String name;
const EventCalanderScreen({super.key, @PathParam("name") required this.name});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Get the current date
final now = DateTime.now();
// Create the query for the current month
final query = useState(
EventCalendarQuery(uname: name, year: now.year, month: now.month),
);
// Watch the event calendar data
final events = ref.watch(eventCalendarProvider(query.value));
final user = ref.watch(accountProvider(name));
// Track the selected day for synchronizing between widgets
final selectedDay = useState(now);
void onMonthChanged(int year, int month) {
query.value = EventCalendarQuery(
uname: query.value.uname,
year: year,
month: month,
);
}
// Function to handle day selection for synchronizing between widgets
void onDaySelected(DateTime day) {
selectedDay.value = day;
}
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('eventCalander').tr(),
),
body: SingleChildScrollView(
child:
MediaQuery.of(context).size.width > 480
? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 480),
child: Column(
children: [
Card(
margin: EdgeInsets.all(16),
child: Column(
children: [
// Use the reusable EventCalendarWidget
EventCalendarWidget(
events: events,
initialDate: now,
showEventDetails: true,
onMonthChanged: onMonthChanged,
onDaySelected: onDaySelected,
),
],
),
),
// Add the fortune graph widget
const Divider(height: 1),
FortuneGraphWidget(
events: events,
constrainWidth: true,
onPointSelected: onDaySelected,
),
// Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue)
AccountNameplate(name: name),
],
),
).center()
: Column(
children: [
// Use the reusable EventCalendarWidget
EventCalendarWidget(
events: events,
initialDate: now,
showEventDetails: true,
onMonthChanged: onMonthChanged,
onDaySelected: onDaySelected,
),
// Add the fortune graph widget
const Divider(height: 1),
FortuneGraphWidget(
events: events,
onPointSelected: onDaySelected,
).padding(horizontal: 8, vertical: 4),
// Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue)
AccountNameplate(name: name),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
),
);
}
}

View File

@ -0,0 +1,258 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
@RoutePage()
class LevelingScreen extends HookConsumerWidget {
const LevelingScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
if (user.value == null) {
return AppScaffold(
appBar: AppBar(title: Text('levelingProgress'.tr())),
body: const Center(child: CircularProgressIndicator()),
);
}
final currentLevel = user.value!.profile.level;
final currentExp = user.value!.profile.experience;
final progress = user.value!.profile.levelingProgress;
return AppScaffold(
appBar: AppBar(title: Text('levelingProgress'.tr())),
body: SingleChildScrollView(
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Current Progress Card
LevelingProgressCard(
level: currentLevel,
experience: currentExp,
progress: progress,
),
const Gap(24),
// Level Stairs Graph
Text(
'Level Progress',
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const Gap(16),
// Stairs visualization with fixed height and horizontal scroll
_buildLevelStairs(context, currentLevel),
const Gap(24),
// Placeholder for unlocked content
Text(
'Unlocked Features',
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const Gap(16),
Container(
height: 200,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Center(
child: Text(
'Unlocked features will be shown here',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
);
}
Widget _buildLevelStairs(BuildContext context, int currentLevel) {
const totalLevels = 14;
const stairHeight = 20.0;
const stairWidth = 50.0;
const containerHeight = 280.0;
return Container(
height: containerHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: (totalLevels * (stairWidth + 8)) + 40,
height: containerHeight,
child: CustomPaint(
painter: LevelStairsPainter(
currentLevel: currentLevel,
totalLevels: totalLevels,
primaryColor: Theme.of(context).colorScheme.primary,
surfaceColor: Theme.of(context).colorScheme.surfaceContainerHigh,
onSurfaceColor: Theme.of(context).colorScheme.onSurface,
stairHeight: stairHeight,
stairWidth: stairWidth,
),
child: Stack(
children: List.generate(totalLevels, (index) {
final level = index + 1;
final isCompleted = level <= currentLevel;
final isCurrent = level == currentLevel;
// Calculate position from bottom
final bottomPosition = 0.0;
final leftPosition = 20.0 + (index * (stairWidth + 8));
// Make higher levels progressively taller
final progressiveHeight =
40.0 + (index * 15.0); // Base height + progressive increase
return Positioned(
left: leftPosition,
bottom: bottomPosition,
child: Container(
width: stairWidth,
height: progressiveHeight,
decoration: BoxDecoration(
color:
isCompleted
? Theme.of(context).colorScheme.primary
: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(6),
topRight: Radius.circular(6),
),
border:
isCurrent
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 2,
)
: null,
boxShadow:
isCurrent
? [
BoxShadow(
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.3),
blurRadius: 6,
spreadRadius: 1,
),
]
: null,
),
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
children: [
Text(
level.toString(),
style: GoogleFonts.robotoMono(
fontSize: 14,
fontWeight: FontWeight.bold,
color:
isCompleted
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurface,
),
),
if (isCurrent) ...[
const Gap(4),
Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onPrimary,
shape: BoxShape.circle,
),
),
],
],
),
),
),
);
}),
),
),
),
),
);
}
}
class LevelStairsPainter extends CustomPainter {
final int currentLevel;
final int totalLevels;
final Color primaryColor;
final Color surfaceColor;
final Color onSurfaceColor;
final double stairHeight;
final double stairWidth;
LevelStairsPainter({
required this.currentLevel,
required this.totalLevels,
required this.primaryColor,
required this.surfaceColor,
required this.onSurfaceColor,
required this.stairHeight,
required this.stairWidth,
});
@override
void paint(Canvas canvas, Size size) {
final paint =
Paint()
..color = surfaceColor.withOpacity(0.2)
..strokeWidth = 1.5
..style = PaintingStyle.stroke;
// Draw connecting lines between stairs
for (int i = 0; i < totalLevels - 1; i++) {
final startX = 20.0 + (i * (stairWidth + 8)) + stairWidth;
final startHeight =
40.0 + (i * 15.0); // Progressive height for current stair
final startY = size.height - (20.0 + startHeight);
final endX = 20.0 + ((i + 1) * (stairWidth + 8));
final endHeight =
40.0 + ((i + 1) * 15.0); // Progressive height for next stair
final endY = size.height - (20.0 + endHeight);
canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -1,232 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:table_calendar/table_calendar.dart';
part 'event_calendar.g.dart';
part 'event_calendar.freezed.dart';
@freezed
sealed class EventCalendarQuery with _$EventCalendarQuery {
const factory EventCalendarQuery({
required String? uname,
required int year,
required int month,
}) = _EventCalendarQuery;
}
@riverpod
Future<List<SnEventCalendarEntry>> accountEventCalendar(
Ref ref,
EventCalendarQuery query,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/accounts/${query.uname ?? 'me'}/calendar');
return resp.data
.map((e) => SnEventCalendarEntry.fromJson(e))
.cast<SnEventCalendarEntry>()
.toList();
}
@RoutePage()
class EventCalanderScreen extends HookConsumerWidget {
final String name;
const EventCalanderScreen({super.key, @PathParam("name") required this.name});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedMonth = useState(DateTime.now().month);
final selectedYear = useState(DateTime.now().year);
final selectedDay = useState(DateTime.now());
final user = ref.watch(accountProvider(name));
final events = ref.watch(
accountEventCalendarProvider(
EventCalendarQuery(
uname: name,
year: selectedYear.value,
month: selectedMonth.value,
),
),
);
final content = Column(
children: [
TableCalendar(
locale: EasyLocalization.of(context)!.locale.toString(),
firstDay: DateTime.now().add(Duration(days: -3650)),
lastDay: DateTime.now().add(Duration(days: 3650)),
focusedDay: DateTime.utc(
selectedYear.value,
selectedMonth.value,
DateTime.now().day,
),
calendarFormat: CalendarFormat.month,
selectedDayPredicate: (day) {
return isSameDay(selectedDay.value, day);
},
onDaySelected: (value, _) {
selectedDay.value = value;
},
onPageChanged: (focusedDay) {
selectedMonth.value = focusedDay.month;
selectedYear.value = focusedDay.year;
},
eventLoader: (day) {
return events.value
?.where((e) => isSameDay(e.date, day))
.expand((e) => [...e.statuses, e.checkInResult])
.where((e) => e != null)
.toList() ??
[];
},
calendarBuilders: CalendarBuilders(
dowBuilder: (context, day) {
final text = DateFormat.EEEEE().format(day);
return Center(child: Text(text));
},
markerBuilder: (context, day, events) {
var checkInResult =
events.whereType<SnCheckInResult>().firstOrNull;
if (checkInResult != null) {
return Positioned(
top: 32,
child: Text(
['大凶', '', '中平', '', '大吉'][checkInResult.level],
style: TextStyle(
fontSize: 9,
color:
isSameDay(selectedDay.value, day)
? Theme.of(context).colorScheme.onPrimaryContainer
: isSameDay(DateTime.now(), day)
? Theme.of(
context,
).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
return null;
},
),
),
const Divider(height: 1).padding(top: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Builder(
builder: (context) {
final event =
events.value
?.where((e) => isSameDay(e.date, selectedDay.value))
.firstOrNull;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(DateFormat.EEEE().format(selectedDay.value))
.fontSize(16)
.bold()
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Text(DateFormat.yMd().format(selectedDay.value))
.fontSize(12)
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
const Gap(16),
if (event?.checkInResult != null)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'checkInResultLevel${event!.checkInResult!.level}',
).tr().fontSize(16).bold(),
for (final tip in event.checkInResult!.tips)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Icon(
Symbols.circle,
size: 12,
fill: 1,
).padding(top: 4, right: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(tip.title).bold(),
Text(tip.content),
],
),
),
],
).padding(top: 8),
],
),
if (event?.checkInResult == null &&
(event?.statuses.isEmpty ?? true))
Text('eventCalanderEmpty').tr(),
],
).padding(vertical: 24, horizontal: 24);
},
),
),
if (name != 'me' && user.hasValue)
Container(
decoration: BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
margin: EdgeInsets.all(16),
child: Card(
margin: EdgeInsets.zero,
elevation: 0,
color: Colors.transparent,
child: ListTile(
leading: ProfilePictureWidget(
fileId: user.value!.profile.picture?.id,
),
title: Text(user.value!.nick).bold(),
subtitle: Text('@${user.value!.name}'),
),
),
),
],
);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('eventCalander').tr(),
),
body: SingleChildScrollView(
child:
MediaQuery.of(context).size.width > 480
? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 480),
child: Card(margin: EdgeInsets.all(16), child: content),
).center()
: content,
),
);
}
}

View File

@ -1,148 +0,0 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'event_calendar.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$EventCalendarQuery {
String? get uname; int get year; int get month;
/// Create a copy of EventCalendarQuery
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$EventCalendarQueryCopyWith<EventCalendarQuery> get copyWith => _$EventCalendarQueryCopyWithImpl<EventCalendarQuery>(this as EventCalendarQuery, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is EventCalendarQuery&&(identical(other.uname, uname) || other.uname == uname)&&(identical(other.year, year) || other.year == year)&&(identical(other.month, month) || other.month == month));
}
@override
int get hashCode => Object.hash(runtimeType,uname,year,month);
@override
String toString() {
return 'EventCalendarQuery(uname: $uname, year: $year, month: $month)';
}
}
/// @nodoc
abstract mixin class $EventCalendarQueryCopyWith<$Res> {
factory $EventCalendarQueryCopyWith(EventCalendarQuery value, $Res Function(EventCalendarQuery) _then) = _$EventCalendarQueryCopyWithImpl;
@useResult
$Res call({
String? uname, int year, int month
});
}
/// @nodoc
class _$EventCalendarQueryCopyWithImpl<$Res>
implements $EventCalendarQueryCopyWith<$Res> {
_$EventCalendarQueryCopyWithImpl(this._self, this._then);
final EventCalendarQuery _self;
final $Res Function(EventCalendarQuery) _then;
/// Create a copy of EventCalendarQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? uname = freezed,Object? year = null,Object? month = null,}) {
return _then(_self.copyWith(
uname: freezed == uname ? _self.uname : uname // ignore: cast_nullable_to_non_nullable
as String?,year: null == year ? _self.year : year // ignore: cast_nullable_to_non_nullable
as int,month: null == month ? _self.month : month // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _EventCalendarQuery implements EventCalendarQuery {
const _EventCalendarQuery({required this.uname, required this.year, required this.month});
@override final String? uname;
@override final int year;
@override final int month;
/// Create a copy of EventCalendarQuery
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$EventCalendarQueryCopyWith<_EventCalendarQuery> get copyWith => __$EventCalendarQueryCopyWithImpl<_EventCalendarQuery>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _EventCalendarQuery&&(identical(other.uname, uname) || other.uname == uname)&&(identical(other.year, year) || other.year == year)&&(identical(other.month, month) || other.month == month));
}
@override
int get hashCode => Object.hash(runtimeType,uname,year,month);
@override
String toString() {
return 'EventCalendarQuery(uname: $uname, year: $year, month: $month)';
}
}
/// @nodoc
abstract mixin class _$EventCalendarQueryCopyWith<$Res> implements $EventCalendarQueryCopyWith<$Res> {
factory _$EventCalendarQueryCopyWith(_EventCalendarQuery value, $Res Function(_EventCalendarQuery) _then) = __$EventCalendarQueryCopyWithImpl;
@override @useResult
$Res call({
String? uname, int year, int month
});
}
/// @nodoc
class __$EventCalendarQueryCopyWithImpl<$Res>
implements _$EventCalendarQueryCopyWith<$Res> {
__$EventCalendarQueryCopyWithImpl(this._self, this._then);
final _EventCalendarQuery _self;
final $Res Function(_EventCalendarQuery) _then;
/// Create a copy of EventCalendarQuery
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? uname = freezed,Object? year = null,Object? month = null,}) {
return _then(_EventCalendarQuery(
uname: freezed == uname ? _self.uname : uname // ignore: cast_nullable_to_non_nullable
as String?,year: null == year ? _self.year : year // ignore: cast_nullable_to_non_nullable
as int,month: null == month ? _self.month : month // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
// dart format on

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:auto_route/annotations.dart';
@ -6,24 +5,22 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/account/me/settings_auth_factors.dart';
import 'package:island/screens/account/me/settings_connections.dart';
import 'package:island/screens/account/me/settings_contacts.dart';
import 'package:island/screens/auth/captcha.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_session_sheet.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@ -45,6 +42,15 @@ Future<List<SnContactMethod>> contactMethods(Ref ref) async {
.toList();
}
@riverpod
Future<List<SnAccountConnection>> accountConnections(Ref ref) async {
final client = ref.read(apiClientProvider);
final resp = await client.get('/accounts/me/connections');
return resp.data
.map<SnAccountConnection>((e) => SnAccountConnection.fromJson(e))
.toList();
}
@RoutePage()
class AccountSettingsScreen extends HookConsumerWidget {
const AccountSettingsScreen({super.key});
@ -86,7 +92,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
if (captchaTk == null) return;
try {
showLoadingModal(context);
if (context.mounted) showLoadingModal(context);
final userInfo = ref.read(userInfoProvider);
final client = ref.read(apiClientProvider);
await client.post(
@ -122,6 +128,96 @@ class AccountSettingsScreen extends HookConsumerWidget {
);
},
),
ExpansionTile(
leading: const Icon(
Symbols.link,
).alignment(Alignment.centerLeft).width(48),
title: Text('accountConnections').tr(),
subtitle: Text('accountConnectionsDescription').tr().fontSize(12),
tilePadding: const EdgeInsets.only(left: 24, right: 17),
children: [
ref
.watch(accountConnectionsProvider)
.when(
data:
(connections) => Column(
children: [
for (final connection in connections)
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 16,
right: 17,
top: 2,
bottom: 4,
),
title:
Text(
getLocalizedProviderName(connection.provider),
).tr(),
subtitle:
connection.meta['email'] != null
? Text(connection.meta['email'])
: Text(connection.providedIdentifier),
leading: CircleAvatar(
child: getProviderIcon(
connection.provider,
size: 16,
color:
Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) => AccountConnectionSheet(
connection: connection,
),
).then((value) {
if (value == true) {
ref.invalidate(accountConnectionsProvider);
}
});
},
),
if (connections.isNotEmpty) const Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('accountConnectionAdd').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) =>
const AccountConnectionNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(accountConnectionsProvider);
}
});
},
),
],
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(accountConnectionsProvider),
),
loading: () => const ResponseLoadingWidget(),
),
],
),
ExpansionTile(
leading: const Icon(
Symbols.security,
@ -184,7 +280,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
showModalBottomSheet(
context: context,
builder:
(context) => _AuthFactorSheet(factor: factor),
(context) => AuthFactorSheet(factor: factor),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
@ -205,7 +301,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => const _AuthFactorNewSheet(),
builder: (context) => const AuthFactorNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
@ -289,7 +385,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
context: context,
builder:
(context) =>
_ContactMethodSheet(contact: contact),
ContactMethodSheet(contact: contact),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
@ -311,7 +407,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
showModalBottomSheet(
context: context,
builder:
(context) => const _ContactMethodNewSheet(),
(context) => const ContactMethodNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
@ -471,599 +567,3 @@ class _SettingsSection extends StatelessWidget {
);
}
}
class _AuthFactorSheet extends HookConsumerWidget {
final SnAuthFactor factor;
const _AuthFactorSheet({required this.factor});
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> deleteFactor() async {
final confirm = await showConfirmAlert(
'authFactorDeleteHint'.tr(),
'authFactorDelete'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.delete('/accounts/me/factors/${factor.id}');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> disableFactor() async {
final confirm = await showConfirmAlert(
'authFactorDisableHint'.tr(),
'authFactorDisable'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post('/accounts/me/factors/${factor.id}/disable');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> enableFactor() async {
String? password;
if ([3].contains(factor.type)) {
final confirmed = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: Text('authFactorEnable').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('authFactorEnableHint').tr(),
const SizedBox(height: 16),
OtpTextField(
showCursor: false,
numberOfFields: 6,
obscureText: false,
showFieldAsBox: true,
focusedBorderColor: Theme.of(context).colorScheme.primary,
onSubmit: (String verificationCode) {
password = verificationCode;
},
textStyle: Theme.of(context).textTheme.titleLarge!,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('cancel').tr(),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('confirm').tr(),
),
],
),
);
if (confirmed == false ||
(password?.isEmpty ?? true) ||
!context.mounted) {
return;
}
}
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post(
'/accounts/me/factors/${factor.id}/enable',
data: jsonEncode(password),
);
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'authFactor'.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(kFactorTypes[factor.type]!.$3, size: 32),
const Gap(8),
Text(kFactorTypes[factor.type]!.$1).tr(),
const Gap(4),
Text(
kFactorTypes[factor.type]!.$2,
style: Theme.of(context).textTheme.bodySmall,
).tr(),
const Gap(10),
Row(
children: [
if (factor.enabledAt == null)
Badge(
label: Text('authFactorDisabled'.tr()),
textColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
)
else
Badge(
label: Text('authFactorEnabled'.tr()),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
],
),
],
).padding(all: 20),
const Divider(height: 1),
if (factor.enabledAt != null)
ListTile(
leading: const Icon(Symbols.disabled_by_default),
title: Text('authFactorDisable').tr(),
onTap: disableFactor,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
)
else
ListTile(
leading: const Icon(Symbols.check_circle),
title: Text('authFactorEnable').tr(),
onTap: enableFactor,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
ListTile(
leading: const Icon(Symbols.delete),
title: Text('authFactorDelete').tr(),
onTap: deleteFactor,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
],
),
);
}
}
class _AuthFactorNewSheet extends HookConsumerWidget {
const _AuthFactorNewSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final factorType = useState<int>(0);
final secretController = useTextEditingController();
Future<void> addFactor() async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
final resp = await apiClient.post(
'/accounts/me/factors',
data: {'type': factorType.value, 'secret': secretController.text},
);
final factor = SnAuthFactor.fromJson(resp.data);
if (!context.mounted) return;
hideLoadingModal(context);
if (factor.type == 3) {
showModalBottomSheet(
context: context,
builder: (context) => _AuthFactorNewAdditonalSheet(factor: factor),
).then((_) {
if (context.mounted) {
showSnackBar(context, 'contactMethodVerificationNeeded'.tr());
}
if (context.mounted) Navigator.pop(context, true);
});
} else {
Navigator.pop(context, true);
}
} catch (err) {
showErrorAlert(err);
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'authFactorNew'.tr(),
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<int>(
value: factorType.value,
decoration: InputDecoration(
labelText: 'authFactor'.tr(),
border: const OutlineInputBorder(),
),
items:
kFactorTypes.entries.map((entry) {
return DropdownMenuItem<int>(
value: entry.key,
child: Row(
children: [
Icon(entry.value.$3),
const Gap(8),
Text(entry.value.$1).tr(),
],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
factorType.value = value;
}
},
),
if (factorType.value == 0)
TextField(
controller: secretController,
decoration: InputDecoration(
prefixIcon: const Icon(Symbols.password_2),
labelText: 'authFactorSecret'.tr(),
hintText: 'authFactorSecretHint'.tr(),
border: const OutlineInputBorder(),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(kFactorTypes[factorType.value]!.$2).tr(),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: addFactor,
icon: Icon(Symbols.add),
label: Text('create').tr(),
),
],
),
],
).padding(horizontal: 20, vertical: 24),
);
}
}
class _AuthFactorNewAdditonalSheet extends StatelessWidget {
final SnAuthFactor factor;
const _AuthFactorNewAdditonalSheet({required this.factor});
@override
Widget build(BuildContext context) {
final uri = factor.createdResponse?['uri'];
return SheetScaffold(
titleText: 'authFactorAdditional'.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (uri != null) ...[
const SizedBox(height: 16),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: QrImageView(
data: uri,
version: QrVersions.auto,
size: 200,
backgroundColor: Theme.of(context).colorScheme.surface,
foregroundColor: Theme.of(context).colorScheme.onSurface,
),
),
),
const Gap(16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'authFactorQrCodeScan'.tr(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
),
] else ...[
const SizedBox(height: 16),
Center(
child: Text(
'authFactorNoQrCode'.tr(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
const Gap(16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Symbols.check),
label: Text('next'.tr()),
),
),
],
),
);
}
}
class _ContactMethodSheet extends HookConsumerWidget {
final SnContactMethod contact;
const _ContactMethodSheet({required this.contact});
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> deleteContactMethod() async {
final confirm = await showConfirmAlert(
'contactMethodDeleteHint'.tr(),
'contactMethodDelete'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.delete('/accounts/me/contacts/${contact.id}');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> verifyContactMethod() async {
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post('/accounts/me/contacts/${contact.id}/verify');
if (context.mounted) {
showSnackBar(context, 'contactMethodVerificationSent'.tr());
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> setContactMethodAsPrimary() async {
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post('/accounts/me/contacts/${contact.id}/primary');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'contactMethod'.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(switch (contact.type) {
0 => Symbols.mail,
1 => Symbols.phone,
_ => Symbols.home,
}, size: 32),
const Gap(8),
Text(switch (contact.type) {
0 => 'contactMethodTypeEmail'.tr(),
1 => 'contactMethodTypePhone'.tr(),
_ => 'contactMethodTypeAddress'.tr(),
}),
const Gap(4),
Text(
contact.content,
style: Theme.of(context).textTheme.bodySmall,
),
const Gap(10),
Row(
children: [
if (contact.verifiedAt == null)
Badge(
label: Text('contactMethodUnverified'.tr()),
textColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
)
else
Badge(
label: Text('contactMethodVerified'.tr()),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
if (contact.isPrimary)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Badge(
label: Text('contactMethodPrimary'.tr()),
textColor: Theme.of(context).colorScheme.onTertiary,
backgroundColor: Theme.of(context).colorScheme.tertiary,
),
),
],
),
],
).padding(all: 20),
const Divider(height: 1),
if (contact.verifiedAt == null)
ListTile(
leading: const Icon(Symbols.verified),
title: Text('contactMethodVerify').tr(),
onTap: verifyContactMethod,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
if (contact.verifiedAt != null && !contact.isPrimary)
ListTile(
leading: const Icon(Symbols.star),
title: Text('contactMethodSetPrimary').tr(),
onTap: setContactMethodAsPrimary,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
ListTile(
leading: const Icon(Symbols.delete),
title: Text('contactMethodDelete').tr(),
onTap: deleteContactMethod,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
],
),
);
}
}
class _ContactMethodNewSheet extends HookConsumerWidget {
const _ContactMethodNewSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final contactType = useState<int>(0);
final contentController = useTextEditingController();
Future<void> addContactMethod() async {
if (contentController.text.isEmpty) {
showSnackBar(context, 'contactMethodContentEmpty'.tr());
return;
}
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/accounts/me/contacts',
data: {'type': contactType.value, 'content': contentController.text},
);
if (context.mounted) {
showSnackBar(context, 'contactMethodVerificationNeeded'.tr());
Navigator.pop(context, true);
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'contactMethodNew'.tr(),
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<int>(
value: contactType.value,
decoration: InputDecoration(
labelText: 'contactMethodType'.tr(),
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem<int>(
value: 0,
child: Row(
children: [
Icon(Symbols.mail),
const Gap(8),
Text('contactMethodTypeEmail'.tr()),
],
),
),
DropdownMenuItem<int>(
value: 1,
child: Row(
children: [
Icon(Symbols.phone),
const Gap(8),
Text('contactMethodTypePhone'.tr()),
],
),
),
DropdownMenuItem<int>(
value: 2,
child: Row(
children: [
Icon(Symbols.home),
const Gap(8),
Text('contactMethodTypeAddress'.tr()),
],
),
),
],
onChanged: (value) {
if (value != null) {
contactType.value = value;
}
},
),
TextField(
controller: contentController,
decoration: InputDecoration(
prefixIcon: Icon(switch (contactType.value) {
0 => Symbols.mail,
1 => Symbols.phone,
_ => Symbols.home,
}),
labelText: switch (contactType.value) {
0 => 'contactMethodTypeEmail'.tr(),
1 => 'contactMethodTypePhone'.tr(),
_ => 'contactMethodTypeAddress'.tr(),
},
hintText: switch (contactType.value) {
0 => 'contactMethodEmailHint'.tr(),
1 => 'contactMethodPhoneHint'.tr(),
_ => 'contactMethodAddressHint'.tr(),
},
border: const OutlineInputBorder(),
),
keyboardType: switch (contactType.value) {
0 => TextInputType.emailAddress,
1 => TextInputType.phone,
_ => TextInputType.multiline,
},
maxLines: switch (contactType.value) {
2 => 3,
_ => 1,
},
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child:
Text(switch (contactType.value) {
0 => 'contactMethodEmailDescription',
1 => 'contactMethodPhoneDescription',
_ => 'contactMethodAddressDescription',
}).tr(),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: addContactMethod,
icon: Icon(Symbols.add),
label: Text('create').tr(),
),
],
),
],
).padding(horizontal: 20, vertical: 24),
);
}
}

View File

@ -44,5 +44,26 @@ final contactMethodsProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ContactMethodsRef = AutoDisposeFutureProviderRef<List<SnContactMethod>>;
String _$accountConnectionsHash() =>
r'38a309d596e0ea2539cd92ea86984e1e4fb346e4';
/// See also [accountConnections].
@ProviderFor(accountConnections)
final accountConnectionsProvider =
AutoDisposeFutureProvider<List<SnAccountConnection>>.internal(
accountConnections,
name: r'accountConnectionsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountConnectionsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AccountConnectionsRef =
AutoDisposeFutureProviderRef<List<SnAccountConnection>>;
// 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

View File

@ -0,0 +1,359 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:styled_widget/styled_widget.dart';
class AuthFactorSheet extends HookConsumerWidget {
final SnAuthFactor factor;
const AuthFactorSheet({super.key, required this.factor});
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> deleteFactor() async {
final confirm = await showConfirmAlert(
'authFactorDeleteHint'.tr(),
'authFactorDelete'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.delete('/accounts/me/factors/${factor.id}');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> disableFactor() async {
final confirm = await showConfirmAlert(
'authFactorDisableHint'.tr(),
'authFactorDisable'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post('/accounts/me/factors/${factor.id}/disable');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> enableFactor() async {
String? password;
if ([3].contains(factor.type)) {
final confirmed = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: Text('authFactorEnable').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('authFactorEnableHint').tr(),
const SizedBox(height: 16),
OtpTextField(
showCursor: false,
numberOfFields: 6,
obscureText: false,
showFieldAsBox: true,
focusedBorderColor: Theme.of(context).colorScheme.primary,
onSubmit: (String verificationCode) {
password = verificationCode;
},
textStyle: Theme.of(context).textTheme.titleLarge!,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('cancel').tr(),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('confirm').tr(),
),
],
),
);
if (confirmed == false ||
(password?.isEmpty ?? true) ||
!context.mounted) {
return;
}
}
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post(
'/accounts/me/factors/${factor.id}/enable',
data: jsonEncode(password),
);
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'authFactor'.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(kFactorTypes[factor.type]!.$3, size: 32),
const Gap(8),
Text(kFactorTypes[factor.type]!.$1).tr(),
const Gap(4),
Text(
kFactorTypes[factor.type]!.$2,
style: Theme.of(context).textTheme.bodySmall,
).tr(),
const Gap(10),
Row(
children: [
if (factor.enabledAt == null)
Badge(
label: Text('authFactorDisabled').tr(),
textColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
)
else
Badge(
label: Text('authFactorEnabled').tr(),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
],
),
],
).padding(all: 20),
const Divider(height: 1),
if (factor.enabledAt != null)
ListTile(
leading: const Icon(Symbols.disabled_by_default),
title: Text('authFactorDisable').tr(),
onTap: disableFactor,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
)
else
ListTile(
leading: const Icon(Symbols.check_circle),
title: Text('authFactorEnable').tr(),
onTap: enableFactor,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
ListTile(
leading: const Icon(Symbols.delete),
title: Text('authFactorDelete').tr(),
onTap: deleteFactor,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
],
),
);
}
}
class AuthFactorNewSheet extends HookConsumerWidget {
const AuthFactorNewSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final factorType = useState<int>(0);
final secretController = useTextEditingController();
Future<void> addFactor() async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
final resp = await apiClient.post(
'/accounts/me/factors',
data: {'type': factorType.value, 'secret': secretController.text},
);
final factor = SnAuthFactor.fromJson(resp.data);
if (!context.mounted) return;
hideLoadingModal(context);
if (factor.type == 3) {
showModalBottomSheet(
context: context,
builder: (context) => AuthFactorNewAdditonalSheet(factor: factor),
).then((_) {
if (context.mounted) {
showSnackBar(context, 'contactMethodVerificationNeeded'.tr());
}
if (context.mounted) Navigator.pop(context, true);
});
} else {
Navigator.pop(context, true);
}
} catch (err) {
showErrorAlert(err);
if (context.mounted) hideLoadingModal(context);
}
}
final width = math.min(400, MediaQuery.of(context).size.width);
return SheetScaffold(
titleText: 'authFactorNew'.tr(),
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<int>(
value: factorType.value,
decoration: InputDecoration(
labelText: 'authFactor'.tr(),
border: const OutlineInputBorder(),
),
items:
kFactorTypes.entries.map((entry) {
return DropdownMenuItem<int>(
value: entry.key,
child: Row(
children: [
Icon(entry.value.$3),
const Gap(8),
Text(entry.value.$1).tr(),
],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
factorType.value = value;
}
},
),
if ([0].contains(factorType.value))
TextField(
controller: secretController,
decoration: InputDecoration(
prefixIcon: const Icon(Symbols.password_2),
labelText: 'authFactorSecret'.tr(),
hintText: 'authFactorSecretHint'.tr(),
border: const OutlineInputBorder(),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
)
else if ([4].contains(factorType.value))
OtpTextField(
showCursor: false,
numberOfFields: 6,
obscureText: false,
showFieldAsBox: true,
focusedBorderColor: Theme.of(context).colorScheme.primary,
fieldWidth: (width / 6) - 10,
keyboardType: TextInputType.number,
onSubmit: (String verificationCode) {
secretController.text = verificationCode;
},
textStyle: Theme.of(context).textTheme.titleLarge!,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(kFactorTypes[factorType.value]!.$2).tr(),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: addFactor,
icon: Icon(Symbols.add),
label: Text('create').tr(),
),
],
),
],
).padding(horizontal: 20, vertical: 24),
);
}
}
class AuthFactorNewAdditonalSheet extends StatelessWidget {
final SnAuthFactor factor;
const AuthFactorNewAdditonalSheet({super.key, required this.factor});
@override
Widget build(BuildContext context) {
final uri = factor.createdResponse?['uri'];
return SheetScaffold(
titleText: 'authFactorAdditional'.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (uri != null) ...[
const SizedBox(height: 16),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: QrImageView(
data: uri,
version: QrVersions.auto,
size: 200,
backgroundColor: Theme.of(context).colorScheme.surface,
foregroundColor: Theme.of(context).colorScheme.onSurface,
),
),
),
const Gap(16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'authFactorQrCodeScan'.tr(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
),
] else ...[
const SizedBox(height: 16),
Center(
child: Text(
'authFactorNoQrCode'.tr(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
const Gap(16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Symbols.check),
label: Text('next'.tr()),
),
),
],
),
);
}
}

View File

@ -0,0 +1,392 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/account/me/settings.dart';
import 'package:island/screens/auth/oidc.native.dart';
import 'package:island/services/text.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:styled_widget/styled_widget.dart';
// Helper function to get provider icon and localized name
Widget getProviderIcon(String provider, {double size = 24, Color? color}) {
final providerLower = provider.toLowerCase();
// Check if we have an SVG for this provider
switch (providerLower) {
case 'apple':
case 'microsoft':
case 'google':
case 'github':
case 'discord':
case 'afdian':
return SvgPicture.asset(
'assets/images/oidc/$providerLower.svg',
width: size,
height: size,
color: color,
);
default:
return Icon(Symbols.link, size: size);
}
}
String getLocalizedProviderName(String provider) {
switch (provider.toLowerCase()) {
case 'apple':
return 'accountConnectionProviderApple'.tr();
case 'microsoft':
return 'accountConnectionProviderMicrosoft'.tr();
case 'google':
return 'accountConnectionProviderGoogle'.tr();
case 'github':
return 'accountConnectionProviderGithub'.tr();
case 'discord':
return 'accountConnectionProviderDiscord'.tr();
case 'afdian':
return 'accountConnectionProviderAfdian'.tr();
default:
return provider;
}
}
class AccountConnectionSheet extends HookConsumerWidget {
final SnAccountConnection connection;
const AccountConnectionSheet({super.key, required this.connection});
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> deleteConnection() async {
final confirm = await showConfirmAlert(
'accountConnectionDeleteHint'.tr(),
'accountConnectionDelete'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.delete('/accounts/me/connections/${connection.id}');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'accountConnections'.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
getProviderIcon(
connection.provider,
size: 32,
color: Theme.of(context).colorScheme.onSurface,
),
const Gap(8),
Text(getLocalizedProviderName(connection.provider)).tr(),
const Gap(4),
if (connection.meta.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
for (final meta in connection.meta.entries)
Text(
'${meta.key.replaceAll('_', ' ').capitalizeEachWord()}: ${meta.value}',
style: const TextStyle(fontSize: 12),
),
],
),
Text(
connection.providedIdentifier,
style: Theme.of(context).textTheme.bodySmall,
),
const Gap(8),
Text(
connection.lastUsedAt.formatSystem(),
style: Theme.of(context).textTheme.bodySmall,
).opacity(0.85),
],
).padding(all: 20),
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.delete),
title: Text('accountConnectionDelete').tr(),
onTap: deleteConnection,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
),
],
),
);
}
}
class AccountConnectionNewSheet extends HookConsumerWidget {
const AccountConnectionNewSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedProvider = useState<String>('apple');
// List of available providers
final providers = [
'apple',
'microsoft',
'google',
'github',
'discord',
'afdian',
];
Future<void> addConnection() async {
final client = ref.watch(apiClientProvider);
switch (selectedProvider.value.toLowerCase()) {
case 'apple':
try {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [AppleIDAuthorizationScopes.email],
webAuthenticationOptions: WebAuthenticationOptions(
clientId: 'dev.solsynth.solarpass',
redirectUri: Uri.parse(
'https://nt.solian.app/auth/callback/apple',
),
),
);
if (context.mounted) showLoadingModal(context);
await client.post(
'/auth/connect/apple/mobile',
data: {
'identity_token': credential.identityToken!,
'authorization_code': credential.authorizationCode,
},
);
if (context.mounted) {
showSnackBar(context, 'accountConnectionAddSuccess'.tr());
Navigator.pop(context, true);
}
} catch (err) {
if (err is SignInWithAppleAuthorizationException) return;
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
case 'microsoft':
case 'google':
case 'github':
case 'discord':
case 'afdian':
await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder:
(context) => OidcScreen(
provider: selectedProvider.value.toLowerCase(),
title:
'Connect with ${selectedProvider.value.capitalizeEachWord()}',
),
),
);
if (context.mounted) Navigator.pop(context, true);
break;
default:
showSnackBar(context, 'accountConnectionAddError'.tr());
return;
}
}
return SheetScaffold(
titleText: 'accountConnectionAdd'.tr(),
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<String>(
value: selectedProvider.value,
decoration: InputDecoration(
prefixIcon: getProviderIcon(
selectedProvider.value,
size: 16,
color: Theme.of(context).colorScheme.onSurface,
).padding(all: 16),
labelText: 'accountConnectionProvider'.tr(),
border: const OutlineInputBorder(),
),
items:
providers.map((String provider) {
return DropdownMenuItem<String>(
value: provider,
child: Row(
children: [Text(getLocalizedProviderName(provider)).tr()],
),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
selectedProvider.value = newValue;
}
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text('accountConnectionDescription'.tr()),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: addConnection,
icon: const Icon(Symbols.add),
label: Text('next').tr(),
),
],
),
],
).padding(horizontal: 20, vertical: 24),
);
}
}
class AccountConnectionsSheet extends HookConsumerWidget {
const AccountConnectionsSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final connections = ref.watch(accountConnectionsProvider);
return SheetScaffold(
titleText: 'accountConnections'.tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () async {
final result = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
builder: (context) => const AccountConnectionNewSheet(),
);
if (result == true) {
ref.invalidate(accountConnectionsProvider);
}
},
),
],
child: connections.when(
data:
(data) => RefreshIndicator(
onRefresh:
() => Future.sync(
() => ref.invalidate(accountConnectionsProvider),
),
child:
data.isEmpty
? Center(
child: Text(
'accountConnectionsEmpty'.tr(),
textAlign: TextAlign.center,
).padding(horizontal: 32),
)
: ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.length,
itemBuilder: (context, index) {
final connection = data[index];
return Dismissible(
key: Key('connection-${connection.id}'),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (direction) async {
final confirm = await showConfirmAlert(
'accountConnectionDeleteHint'.tr(),
'accountConnectionDelete'.tr(),
);
if (confirm && context.mounted) {
try {
final client = ref.read(apiClientProvider);
await client.delete(
'/accounts/me/connections/${connection.id}',
);
ref.invalidate(accountConnectionsProvider);
return true;
} catch (err) {
showErrorAlert(err);
return false;
}
}
return false;
},
child: ListTile(
leading: getProviderIcon(
connection.provider,
color: Theme.of(context).colorScheme.onSurface,
),
title:
Text(
getLocalizedProviderName(
connection.provider,
),
).tr(),
subtitle:
connection.meta['email'] != null
? Text(connection.meta['email'])
: Text(connection.providedIdentifier),
trailing: Text(
DateFormat.yMd().format(
connection.lastUsedAt.toLocal(),
),
style: Theme.of(context).textTheme.bodySmall,
),
onTap: () async {
final result = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
builder:
(context) => AccountConnectionSheet(
connection: connection,
),
);
if (result == true) {
ref.invalidate(accountConnectionsProvider);
}
},
),
);
},
),
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(accountConnectionsProvider),
),
loading: () => const ResponseLoadingWidget(),
),
);
}
}

View File

@ -0,0 +1,281 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class ContactMethodSheet extends HookConsumerWidget {
final SnContactMethod contact;
const ContactMethodSheet({super.key, required this.contact});
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> deleteContactMethod() async {
final confirm = await showConfirmAlert(
'contactMethodDeleteHint'.tr(),
'contactMethodDelete'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.delete('/accounts/me/contacts/${contact.id}');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> verifyContactMethod() async {
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post('/accounts/me/contacts/${contact.id}/verify');
if (context.mounted) {
showSnackBar(context, 'contactMethodVerificationSent'.tr());
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> setContactMethodAsPrimary() async {
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post('/accounts/me/contacts/${contact.id}/primary');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'contactMethod'.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(switch (contact.type) {
0 => Symbols.mail,
1 => Symbols.phone,
_ => Symbols.home,
}, size: 32),
const Gap(8),
Text(switch (contact.type) {
0 => 'contactMethodTypeEmail'.tr(),
1 => 'contactMethodTypePhone'.tr(),
_ => 'contactMethodTypeAddress'.tr(),
}),
const Gap(4),
Text(
contact.content,
style: Theme.of(context).textTheme.bodySmall,
),
const Gap(10),
Row(
children: [
if (contact.verifiedAt == null)
Badge(
label: Text('contactMethodUnverified'.tr()),
textColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
)
else
Badge(
label: Text('contactMethodVerified'.tr()),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
if (contact.isPrimary)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Badge(
label: Text('contactMethodPrimary'.tr()),
textColor: Theme.of(context).colorScheme.onTertiary,
backgroundColor: Theme.of(context).colorScheme.tertiary,
),
),
],
),
],
).padding(all: 20),
const Divider(height: 1),
if (contact.verifiedAt == null)
ListTile(
leading: const Icon(Symbols.verified),
title: Text('contactMethodVerify').tr(),
onTap: verifyContactMethod,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
if (contact.verifiedAt != null && !contact.isPrimary)
ListTile(
leading: const Icon(Symbols.star),
title: Text('contactMethodSetPrimary').tr(),
onTap: setContactMethodAsPrimary,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
ListTile(
leading: const Icon(Symbols.delete),
title: Text('contactMethodDelete').tr(),
onTap: deleteContactMethod,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
],
),
);
}
}
class ContactMethodNewSheet extends HookConsumerWidget {
const ContactMethodNewSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final contactType = useState<int>(0);
final contentController = useTextEditingController();
Future<void> addContactMethod() async {
if (contentController.text.isEmpty) {
showSnackBar(context, 'contactMethodContentEmpty'.tr());
return;
}
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/accounts/me/contacts',
data: {'type': contactType.value, 'content': contentController.text},
);
if (context.mounted) {
showSnackBar(context, 'contactMethodVerificationNeeded'.tr());
Navigator.pop(context, true);
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'contactMethodNew'.tr(),
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<int>(
value: contactType.value,
decoration: InputDecoration(
labelText: 'contactMethodType'.tr(),
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem<int>(
value: 0,
child: Row(
children: [
Icon(Symbols.mail),
const Gap(8),
Text('contactMethodTypeEmail'.tr()),
],
),
),
DropdownMenuItem<int>(
value: 1,
child: Row(
children: [
Icon(Symbols.phone),
const Gap(8),
Text('contactMethodTypePhone'.tr()),
],
),
),
DropdownMenuItem<int>(
value: 2,
child: Row(
children: [
Icon(Symbols.home),
const Gap(8),
Text('contactMethodTypeAddress'.tr()),
],
),
),
],
onChanged: (value) {
if (value != null) {
contactType.value = value;
}
},
),
TextField(
controller: contentController,
decoration: InputDecoration(
prefixIcon: Icon(switch (contactType.value) {
0 => Symbols.mail,
1 => Symbols.phone,
_ => Symbols.home,
}),
labelText: switch (contactType.value) {
0 => 'contactMethodTypeEmail'.tr(),
1 => 'contactMethodTypePhone'.tr(),
_ => 'contactMethodTypeAddress'.tr(),
},
hintText: switch (contactType.value) {
0 => 'contactMethodEmailHint'.tr(),
1 => 'contactMethodPhoneHint'.tr(),
_ => 'contactMethodAddressHint'.tr(),
},
border: const OutlineInputBorder(),
),
keyboardType: switch (contactType.value) {
0 => TextInputType.emailAddress,
1 => TextInputType.phone,
_ => TextInputType.multiline,
},
maxLines: switch (contactType.value) {
2 => 3,
_ => 1,
},
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child:
Text(switch (contactType.value) {
0 => 'contactMethodEmailDescription',
1 => 'contactMethodPhoneDescription',
_ => 'contactMethodAddressDescription',
}).tr(),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: addContactMethod,
icon: Icon(Symbols.add),
label: Text('create').tr(),
),
],
),
],
).padding(horizontal: 20, vertical: 24),
);
}
}

View File

@ -11,6 +11,7 @@ import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/file.dart';
import 'package:island/services/timezone.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
@ -120,9 +121,33 @@ class UpdateProfileScreen extends HookConsumerWidget {
}
final formKeyProfile = useMemoized(GlobalKey<FormState>.new, const []);
final birthday = useState<DateTime?>(
user.value!.profile.birthday?.toLocal(),
);
final firstNameController = useTextEditingController(
text: user.value!.profile.firstName,
);
final middleNameController = useTextEditingController(
text: user.value!.profile.middleName,
);
final lastNameController = useTextEditingController(
text: user.value!.profile.lastName,
);
final bioController = useTextEditingController(
text: user.value!.profile.bio,
);
final genderController = useTextEditingController(
text: user.value!.profile.gender,
);
final pronounsController = useTextEditingController(
text: user.value!.profile.pronouns,
);
final locationController = useTextEditingController(
text: user.value!.profile.location,
);
final timeZoneController = useTextEditingController(
text: user.value!.profile.timeZone,
);
void updateProfile() async {
if (!formKeyProfile.currentState!.validate()) return;
@ -132,7 +157,17 @@ class UpdateProfileScreen extends HookConsumerWidget {
final client = ref.watch(apiClientProvider);
await client.patch(
'/accounts/me/profile',
data: {'bio': bioController.text},
data: {
'bio': bioController.text,
'first_name': firstNameController.text,
'middle_name': middleNameController.text,
'last_name': lastNameController.text,
'gender': genderController.text,
'pronouns': pronounsController.text,
'location': locationController.text,
'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(),
},
);
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
@ -268,6 +303,45 @@ class UpdateProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'firstName'.tr(),
),
controller: firstNameController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'middleName'.tr(),
),
controller: middleNameController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'lastName'.tr(),
),
controller: lastNameController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
),
TextFormField(
decoration: InputDecoration(labelText: 'bio'.tr()),
maxLines: null,
@ -276,6 +350,213 @@ class UpdateProfileScreen extends HookConsumerWidget {
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Row(
spacing: 16,
children: [
Expanded(
child: Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
final options = ['Male', 'Female'];
if (textEditingValue.text == '') {
return options;
}
return options.where(
(option) => option.toLowerCase().contains(
textEditingValue.text.toLowerCase(),
),
);
},
onSelected: (String selection) {
genderController.text = selection;
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Initialize the controller with the current value
if (controller.text.isEmpty &&
genderController.text.isNotEmpty) {
controller.text = genderController.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'gender'.tr(),
),
onChanged: (value) {
genderController.text = value;
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
);
},
),
),
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'pronouns'.tr(),
),
controller: pronounsController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'location'.tr(),
),
controller: locationController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
child: Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<String>.empty();
}
final lowercaseQuery =
textEditingValue.text.toLowerCase();
return getAvailableTz().where((tz) {
return tz.toLowerCase().contains(lowercaseQuery);
});
},
onSelected: (String selection) {
timeZoneController.text = selection;
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Sync the controller with timeZoneController when the widget is built
if (controller.text != timeZoneController.text) {
controller.text = timeZoneController.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
suffix: InkWell(
child: const Icon(
Symbols.my_location,
size: 18,
),
onTap: () async {
try {
showLoadingModal(context);
final machineTz = await getMachineTz();
controller.text = machineTz;
timeZoneController.text = machineTz;
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
),
),
onChanged: (value) {
timeZoneController.text = value;
},
);
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200,
maxWidth: 300,
),
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: options.length,
itemBuilder: (
BuildContext context,
int index,
) {
final option = options.elementAt(index);
return ListTile(
title: Text(
option,
overflow: TextOverflow.ellipsis,
),
onTap: () {
onSelected(option);
},
);
},
),
),
),
);
},
),
),
],
),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: birthday.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) {
birthday.value = date;
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'birthday'.tr(),
style: TextStyle(
color: Theme.of(context).hintColor,
),
),
Text(
birthday.value != null
? DateFormat.yMMMd().format(birthday.value!)
: 'Select a date'.tr(),
),
],
),
),
),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(

View File

@ -1,16 +1,29 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/relationship.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/event_calendar.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/color.dart';
import 'package:island/services/time.dart';
import 'package:island/services/timezone/native.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/fortune_graph.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@ -38,6 +51,51 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
);
}
@riverpod
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
}
@riverpod
Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get("/chat/direct/${account.id}");
return SnChatRoom.fromJson(resp.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null;
}
rethrow;
}
}
@riverpod
Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get("/relationships/${account.id}");
return SnRelationship.fromJson(resp.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null;
}
rethrow;
}
}
@RoutePage()
class AccountProfileScreen extends HookConsumerWidget {
final String name;
@ -48,13 +106,118 @@ class AccountProfileScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final account = ref.watch(accountProvider(name));
final now = DateTime.now();
final iconShadow = Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: const Offset(1.0, 1.0),
final account = ref.watch(accountProvider(name));
final accountEvents = ref.watch(
eventCalendarProvider(
EventCalendarQuery(uname: name, year: now.year, month: now.month),
),
);
final accountChat = ref.watch(accountDirectChatProvider(name));
final accountRelationship = ref.watch(accountRelationshipProvider(name));
final appbarColor = ref.watch(accountAppbarForcegroundColorProvider(name));
final appbarShadow = Shadow(
color: appbarColor.value?.invert ?? Colors.transparent,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
);
Future<void> relationshipAction() async {
if (accountRelationship.value != null) return;
showLoadingModal(context);
try {
final client = ref.watch(apiClientProvider);
await client.post('/relationships/${account.value!.id}/friends');
ref.invalidate(accountRelationshipProvider(name));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> directMessageAction() async {
if (!account.hasValue) return;
if (accountChat.value != null) {
context.router.pushPath('/chat/${accountChat.value!.id}');
return;
}
showLoadingModal(context);
try {
final client = ref.watch(apiClientProvider);
final resp = await client.post(
'/chat/direct',
data: {'related_user_id': account.value!.id},
);
final chat = SnChatRoom.fromJson(resp.data);
if (context.mounted) context.router.pushPath('/chat/${chat.id}');
ref.invalidate(accountDirectChatProvider(name));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
List<Widget> buildSubcolumn(SnAccount data) {
return [
if (data.profile.birthday != null)
Row(
spacing: 6,
children: [
const Icon(Symbols.cake, size: 17, fill: 1),
Text(data.profile.birthday!.formatCustom('yyyy-MM-dd')),
Text('·').bold(),
Text(
'${DateTime.now().difference(data.profile.birthday!).inDays ~/ 365} yrs old',
),
],
),
if (data.profile.location.isNotEmpty)
Row(
spacing: 6,
children: [
const Icon(Symbols.location_on, size: 17, fill: 1),
Text(data.profile.location),
],
),
if (data.profile.pronouns.isNotEmpty || data.profile.gender.isNotEmpty)
Row(
spacing: 6,
children: [
const Icon(Symbols.person, size: 17, fill: 1),
Text(
data.profile.gender.isEmpty
? 'unspecified'.tr()
: data.profile.gender,
),
Text('·').bold(),
Text(
data.profile.pronouns.isEmpty
? 'unspecified'.tr()
: data.profile.pronouns,
),
],
),
if (data.profile.firstName.isNotEmpty ||
data.profile.middleName.isNotEmpty ||
data.profile.lastName.isNotEmpty)
Row(
spacing: 6,
children: [
const Icon(Symbols.id_card, size: 17, fill: 1),
if (data.profile.firstName.isNotEmpty)
Text(data.profile.firstName),
if (data.profile.middleName.isNotEmpty)
Text(data.profile.middleName),
if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName),
],
),
];
}
return account.when(
data:
@ -62,26 +225,40 @@ class AccountProfileScreen extends HookConsumerWidget {
body: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
data.profile.background?.id != null
? CloudImageWidget(
fileId: data.profile.background!.id,
)
: Container(
color:
Theme.of(context).appBarTheme.backgroundColor,
),
title: Text(
data.nick,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
shadows: [iconShadow],
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
),
],
),
),
SliverToBoxAdapter(
@ -91,7 +268,7 @@ class AccountProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: data.profile.picture?.id,
file: data.profile.picture,
radius: 32,
),
const Gap(20),
@ -124,29 +301,144 @@ class AccountProfileScreen extends HookConsumerWidget {
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
)
else
const SliverGap(4),
SliverToBoxAdapter(
child: LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
).padding(horizontal: 20, bottom: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(bottom: 24),
),
if (data.profile.bio.isNotEmpty)
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold(),
Text(data.profile.bio),
],
).padding(horizontal: 24),
),
SliverToBoxAdapter(
child: Column(
spacing: 12,
children: [
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
if (data.profile.verification != null)
VerificationStatusCard(
mark: data.profile.verification!,
),
],
).padding(horizontal: 20),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(vertical: 24),
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 24,
children: [
if (buildSubcolumn(data).isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: buildSubcolumn(data),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('bio').tr().bold(),
Text(
data.profile.bio.isEmpty
? 'descriptionNone'.tr()
: data.profile.bio,
),
],
),
if (data.profile.timeZone.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('timeZone').tr().bold(),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
spacing: 6,
children: [
Text(data.profile.timeZone),
Text(
getTzInfo(
data.profile.timeZone,
).$2.formatCustomGlobal('HH:mm'),
),
Text(
getTzInfo(
data.profile.timeZone,
).$1.formatOffsetLocal(),
).fontSize(11),
Text(
'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}',
).fontSize(11).opacity(0.75),
],
),
],
),
],
).padding(horizontal: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 24, bottom: 12),
),
SliverToBoxAdapter(
child: Row(
spacing: 8,
children: [
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
),
Expanded(
child: FilledButton.icon(
onPressed: directMessageAction,
icon: const Icon(Symbols.message),
label:
Text(
accountChat.value == null
? 'createDirectMessage'
: 'gotoDirectMessage',
maxLines: 1,
).tr(),
),
),
],
).padding(horizontal: 16),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 12),
),
SliverToBoxAdapter(
child: Column(
children: [
FortuneGraphWidget(
events: accountEvents,
eventCalanderUser: data.name,
),
],
).padding(all: 8),
),
],
),
),

View File

@ -267,5 +267,377 @@ class _AccountBadgesProviderElement
String get uname => (origin as AccountBadgesProvider).uname;
}
String _$accountAppbarForcegroundColorHash() =>
r'f654a7a5594eda1500906e9ad023c22772257a9b';
/// See also [accountAppbarForcegroundColor].
@ProviderFor(accountAppbarForcegroundColor)
const accountAppbarForcegroundColorProvider =
AccountAppbarForcegroundColorFamily();
/// See also [accountAppbarForcegroundColor].
class AccountAppbarForcegroundColorFamily extends Family<AsyncValue<Color?>> {
/// See also [accountAppbarForcegroundColor].
const AccountAppbarForcegroundColorFamily();
/// See also [accountAppbarForcegroundColor].
AccountAppbarForcegroundColorProvider call(String uname) {
return AccountAppbarForcegroundColorProvider(uname);
}
@override
AccountAppbarForcegroundColorProvider getProviderOverride(
covariant AccountAppbarForcegroundColorProvider provider,
) {
return call(provider.uname);
}
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'accountAppbarForcegroundColorProvider';
}
/// See also [accountAppbarForcegroundColor].
class AccountAppbarForcegroundColorProvider
extends AutoDisposeFutureProvider<Color?> {
/// See also [accountAppbarForcegroundColor].
AccountAppbarForcegroundColorProvider(String uname)
: this._internal(
(ref) => accountAppbarForcegroundColor(
ref as AccountAppbarForcegroundColorRef,
uname,
),
from: accountAppbarForcegroundColorProvider,
name: r'accountAppbarForcegroundColorProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountAppbarForcegroundColorHash,
dependencies: AccountAppbarForcegroundColorFamily._dependencies,
allTransitiveDependencies:
AccountAppbarForcegroundColorFamily._allTransitiveDependencies,
uname: uname,
);
AccountAppbarForcegroundColorProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
Override overrideWith(
FutureOr<Color?> Function(AccountAppbarForcegroundColorRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountAppbarForcegroundColorProvider._internal(
(ref) => create(ref as AccountAppbarForcegroundColorRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<Color?> createElement() {
return _AccountAppbarForcegroundColorProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountAppbarForcegroundColorProvider &&
other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountAppbarForcegroundColorRef on AutoDisposeFutureProviderRef<Color?> {
/// The parameter `uname` of this provider.
String get uname;
}
class _AccountAppbarForcegroundColorProviderElement
extends AutoDisposeFutureProviderElement<Color?>
with AccountAppbarForcegroundColorRef {
_AccountAppbarForcegroundColorProviderElement(super.provider);
@override
String get uname => (origin as AccountAppbarForcegroundColorProvider).uname;
}
String _$accountDirectChatHash() => r'60d0015fc2a3c8fc2190bb41d6818cf3027d9d0a';
/// See also [accountDirectChat].
@ProviderFor(accountDirectChat)
const accountDirectChatProvider = AccountDirectChatFamily();
/// See also [accountDirectChat].
class AccountDirectChatFamily extends Family<AsyncValue<SnChatRoom?>> {
/// See also [accountDirectChat].
const AccountDirectChatFamily();
/// See also [accountDirectChat].
AccountDirectChatProvider call(String uname) {
return AccountDirectChatProvider(uname);
}
@override
AccountDirectChatProvider getProviderOverride(
covariant AccountDirectChatProvider provider,
) {
return call(provider.uname);
}
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'accountDirectChatProvider';
}
/// See also [accountDirectChat].
class AccountDirectChatProvider extends AutoDisposeFutureProvider<SnChatRoom?> {
/// See also [accountDirectChat].
AccountDirectChatProvider(String uname)
: this._internal(
(ref) => accountDirectChat(ref as AccountDirectChatRef, uname),
from: accountDirectChatProvider,
name: r'accountDirectChatProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountDirectChatHash,
dependencies: AccountDirectChatFamily._dependencies,
allTransitiveDependencies:
AccountDirectChatFamily._allTransitiveDependencies,
uname: uname,
);
AccountDirectChatProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
Override overrideWith(
FutureOr<SnChatRoom?> Function(AccountDirectChatRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountDirectChatProvider._internal(
(ref) => create(ref as AccountDirectChatRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnChatRoom?> createElement() {
return _AccountDirectChatProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountDirectChatProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountDirectChatRef on AutoDisposeFutureProviderRef<SnChatRoom?> {
/// The parameter `uname` of this provider.
String get uname;
}
class _AccountDirectChatProviderElement
extends AutoDisposeFutureProviderElement<SnChatRoom?>
with AccountDirectChatRef {
_AccountDirectChatProviderElement(super.provider);
@override
String get uname => (origin as AccountDirectChatProvider).uname;
}
String _$accountRelationshipHash() =>
r'cb7d0d3f8cd4f23ad9d2d529872c540dac483d4f';
/// See also [accountRelationship].
@ProviderFor(accountRelationship)
const accountRelationshipProvider = AccountRelationshipFamily();
/// See also [accountRelationship].
class AccountRelationshipFamily extends Family<AsyncValue<SnRelationship?>> {
/// See also [accountRelationship].
const AccountRelationshipFamily();
/// See also [accountRelationship].
AccountRelationshipProvider call(String uname) {
return AccountRelationshipProvider(uname);
}
@override
AccountRelationshipProvider getProviderOverride(
covariant AccountRelationshipProvider provider,
) {
return call(provider.uname);
}
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'accountRelationshipProvider';
}
/// See also [accountRelationship].
class AccountRelationshipProvider
extends AutoDisposeFutureProvider<SnRelationship?> {
/// See also [accountRelationship].
AccountRelationshipProvider(String uname)
: this._internal(
(ref) => accountRelationship(ref as AccountRelationshipRef, uname),
from: accountRelationshipProvider,
name: r'accountRelationshipProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountRelationshipHash,
dependencies: AccountRelationshipFamily._dependencies,
allTransitiveDependencies:
AccountRelationshipFamily._allTransitiveDependencies,
uname: uname,
);
AccountRelationshipProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
Override overrideWith(
FutureOr<SnRelationship?> Function(AccountRelationshipRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountRelationshipProvider._internal(
(ref) => create(ref as AccountRelationshipRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnRelationship?> createElement() {
return _AccountRelationshipProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountRelationshipProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountRelationshipRef on AutoDisposeFutureProviderRef<SnRelationship?> {
/// The parameter `uname` of this provider.
String get uname;
}
class _AccountRelationshipProviderElement
extends AutoDisposeFutureProviderElement<SnRelationship?>
with AccountRelationshipRef {
_AccountRelationshipProviderElement(super.provider);
@override
String get uname => (origin as AccountRelationshipProvider).uname;
}
// 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

View File

@ -18,11 +18,14 @@ import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/screens/account/me/settings_connections.dart';
import 'package:island/screens/auth/oidc.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/udid.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -37,6 +40,7 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
Symbols.notifications_active,
),
3: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
};
@RoutePage()
@ -104,7 +108,7 @@ class LoginScreen extends HookConsumerWidget {
child: switch (period.value % 3) {
1 => _LoginPickerScreen(
key: const ValueKey(1),
ticket: currentTicket.value,
challenge: currentTicket.value,
factors: factors.value,
onChallenge:
(SnAuthChallenge? p0) => currentTicket.value = p0,
@ -172,6 +176,89 @@ class _LoginCheckScreen extends HookConsumerWidget {
return null;
}, [isBusy]);
Future<void> getToken({String? code}) async {
// Get token if challenge is completed
final client = ref.watch(apiClientProvider);
final tokenResp = await client.post(
'/auth/token',
data: {
'grant_type': 'authorization_code',
'code': code ?? challenge!.id,
},
);
final token = tokenResp.data['token'];
setToken(ref.watch(sharedPreferencesProvider), token);
ref.invalidate(tokenProvider);
if (!context.mounted) return;
// Do post login tasks
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser().then((_) {
final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient);
final wsNotifier = ref.read(websocketStateProvider.notifier);
wsNotifier.connect();
if (context.mounted) Navigator.pop(context, true);
});
// Update the sessions' device name is available
if (!kIsWeb) {
String? name;
if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
name = deviceInfo.name;
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
name = deviceInfo.name;
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
name = deviceInfo.computerName;
}
if (name != null) {
final client = ref.watch(apiClientProvider);
await client.patch(
'/accounts/me/sessions/current/label',
data: jsonEncode(name),
);
}
}
}
useEffect(() {
if (challenge != null && challenge?.stepRemain == 0) {
Future(() {
isBusy.value = true;
getToken().catchError((err) {
showErrorAlert(err);
isBusy.value = false;
});
});
}
return null;
}, [challenge]);
if (factor == null) {
// Logging in by third parties
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.centerLeft,
child: CircleAvatar(
radius: 26,
child: const Icon(Symbols.asterisk, size: 28),
).padding(bottom: 8),
),
Text(
'loginInProgress'.tr(),
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
).padding(left: 4, bottom: 16),
const Gap(16),
CircularProgressIndicator().alignment(Alignment.centerLeft),
],
);
}
Future<void> performCheckTicket() async {
final pwd = passwordController.value.text;
if (pwd.isEmpty) return;
@ -190,50 +277,7 @@ class _LoginCheckScreen extends HookConsumerWidget {
return;
}
// Get token if challenge is completed
final tokenResp = await client.post(
'/auth/token',
data: {'grant_type': 'authorization_code', 'code': result.id},
);
final token = tokenResp.data['token'];
setToken(ref.watch(sharedPreferencesProvider), token);
ref.invalidate(tokenProvider);
if (!context.mounted) return;
// Do post login tasks
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser().then((_) {
final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient);
final wsNotifier = ref.read(websocketStateProvider.notifier);
wsNotifier.connect();
if (context.mounted) Navigator.pop(context, true);
});
// Update the sessions' device name is available
if (!kIsWeb) {
String? name;
if (Platform.isIOS) {
return;
// TODO waiting for apple to respond to grant my access to com.apple.developer.device-information.user-assigned-device-name
// ignore: dead_code
final deviceInfo = await DeviceInfoPlugin().iosInfo;
name = deviceInfo.name;
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
name = deviceInfo.name;
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
name = deviceInfo.computerName;
}
if (name != null) {
final client = ref.watch(apiClientProvider);
await client.patch(
'/accounts/me/sessions/current/label',
data: jsonEncode(name),
);
}
}
await getToken(code: result.id);
} catch (err) {
showErrorAlert(err);
return;
@ -271,7 +315,6 @@ class _LoginCheckScreen extends HookConsumerWidget {
],
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'password'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
@ -292,14 +335,12 @@ class _LoginCheckScreen extends HookConsumerWidget {
textStyle: Theme.of(context).textTheme.titleLarge!,
),
const Gap(12),
Card(
child: ListTile(
leading: Icon(
kFactorTypes[factor!.type]?.$3 ?? Symbols.question_mark,
),
title: Text(kFactorTypes[factor!.type]?.$1 ?? 'unknown').tr(),
subtitle: Text(kFactorTypes[factor!.type]?.$2 ?? 'unknown').tr(),
ListTile(
leading: Icon(
kFactorTypes[factor!.type]?.$3 ?? Symbols.question_mark,
),
title: Text(kFactorTypes[factor!.type]?.$1 ?? 'unknown').tr(),
subtitle: Text(kFactorTypes[factor!.type]?.$2 ?? 'unknown').tr(),
),
const Gap(12),
Row(
@ -323,7 +364,7 @@ class _LoginCheckScreen extends HookConsumerWidget {
}
class _LoginPickerScreen extends HookConsumerWidget {
final SnAuthChallenge? ticket;
final SnAuthChallenge? challenge;
final List<SnAuthFactor>? factors;
final Function(SnAuthChallenge?) onChallenge;
final Function(SnAuthFactor) onPickFactor;
@ -332,7 +373,7 @@ class _LoginPickerScreen extends HookConsumerWidget {
const _LoginPickerScreen({
super.key,
required this.ticket,
required this.challenge,
required this.factors,
required this.onChallenge,
required this.onPickFactor,
@ -350,6 +391,15 @@ class _LoginPickerScreen extends HookConsumerWidget {
return null;
}, [isBusy]);
useEffect(() {
if (challenge != null && challenge?.stepRemain == 0) {
Future(() {
onNext();
});
}
return null;
}, [challenge]);
final unfocusColor = Theme.of(
context,
).colorScheme.onSurface.withAlpha((255 * 0.75).round());
@ -364,7 +414,7 @@ class _LoginPickerScreen extends HookConsumerWidget {
try {
await client.post(
'/auth/challenge/${ticket!.id}/factors/${factorPicked.value!.id}',
'/auth/challenge/${challenge!.id}/factors/${factorPicked.value!.id}',
data:
hintController.text.isNotEmpty
? jsonEncode(hintController.text)
@ -388,20 +438,6 @@ class _LoginPickerScreen extends HookConsumerWidget {
}
}
useEffect(() {
if (ticket == null || (factors?.isEmpty ?? true)) return;
if (ticket!.blacklistFactors.isEmpty) {
Future(() {
var password = factors!.where((x) => x.type == 0).firstOrNull;
if (password != null) {
factorPicked.value = password;
performGetFactorCode();
}
});
}
return null;
}, [ticket, factors]);
return Column(
key: const ValueKey<int>(1),
crossAxisAlignment: CrossAxisAlignment.start,
@ -432,7 +468,7 @@ class _LoginPickerScreen extends HookConsumerWidget {
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
),
title: Text(kFactorTypes[x.type]?.$1 ?? 'unknown').tr(),
enabled: !ticket!.blacklistFactors.contains(x.id),
enabled: !challenge!.blacklistFactors.contains(x.id),
value: factorPicked.value == x,
onChanged: (value) {
if (value == true) {
@ -457,7 +493,7 @@ class _LoginPickerScreen extends HookConsumerWidget {
).padding(top: 12, bottom: 4, horizontal: 4),
const Gap(8),
Text(
'loginMultiFactor'.plural(ticket!.stepRemain),
'loginMultiFactor'.plural(challenge!.stepRemain),
style: TextStyle(color: unfocusColor, fontSize: 13),
).padding(horizontal: 16),
const Gap(12),
@ -575,6 +611,72 @@ class _LoginLookupScreen extends HookConsumerWidget {
}
}
Future<void> withApple() async {
final client = ref.watch(apiClientProvider);
try {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [AppleIDAuthorizationScopes.email],
webAuthenticationOptions: WebAuthenticationOptions(
clientId: 'dev.solsynth.solarpass',
redirectUri: Uri.parse('https://nt.solian.app/auth/callback/apple'),
),
);
if (context.mounted) showLoadingModal(context);
final resp = await client.post(
'/auth/login/apple/mobile',
data: {
'identity_token': credential.identityToken!,
'authorization_code': credential.authorizationCode,
'device_id': await getUdid(),
},
);
final challenge = SnAuthChallenge.fromJson(resp.data);
onChallenge(challenge);
final factorResp = await client.get(
'/auth/challenge/${challenge.id}/factors',
);
onFactor(
List<SnAuthFactor>.from(
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
),
);
onNext();
} catch (err) {
if (err is SignInWithAppleAuthorizationException) return;
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> withOidc(String provider) async {
final challengeId = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (context) => OidcScreen(provider: provider.toLowerCase()),
),
);
final client = ref.watch(apiClientProvider);
try {
final resp = await client.get('/auth/challenge/$challengeId');
final challenge = SnAuthChallenge.fromJson(resp.data);
onChallenge(challenge);
final factorResp = await client.get(
'/auth/challenge/${challenge.id}/factors',
);
onFactor(
List<SnAuthFactor>.from(
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
),
);
onNext();
} catch (err) {
showErrorAlert(err);
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -603,7 +705,45 @@ class _LoginLookupScreen extends HookConsumerWidget {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: isBusy.value ? null : (_) => performNewTicket(),
).padding(horizontal: 7),
const Gap(12),
Row(
spacing: 6,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("loginOr").tr().fontSize(11).opacity(0.85),
const Gap(8),
Spacer(),
IconButton.filledTonal(
onPressed: () => withOidc('github'),
padding: EdgeInsets.zero,
icon: getProviderIcon(
"github",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
tooltip: 'GitHub',
),
IconButton.filledTonal(
onPressed: () => withOidc('google'),
padding: EdgeInsets.zero,
icon: getProviderIcon(
"google",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
tooltip: 'Google',
),
IconButton.filledTonal(
onPressed: withApple,
padding: EdgeInsets.zero,
icon: getProviderIcon(
"apple",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
tooltip: 'Apple Account',
),
],
).padding(horizontal: 8, vertical: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@ -0,0 +1 @@
export 'oidc.native.dart' if (dart.library.html) 'oidc.web.dart';

View File

@ -0,0 +1,225 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:gap/gap.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/udid.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:styled_widget/styled_widget.dart';
class OidcScreen extends ConsumerStatefulWidget {
final String provider;
final String? title;
const OidcScreen({super.key, required this.provider, this.title});
@override
ConsumerState<OidcScreen> createState() => _OidcScreenState();
}
class _OidcScreenState extends ConsumerState<OidcScreen> {
String? authToken;
String? currentUrl;
final TextEditingController _urlController = TextEditingController();
bool _isLoading = true;
late Future<String> _deviceIdFuture;
@override
void initState() {
super.initState();
_deviceIdFuture = getUdid();
}
@override
void dispose() {
_urlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final serverUrl = ref.watch(serverUrlProvider);
final token = ref.watch(tokenProvider);
return AppScaffold(
appBar: AppBar(
title: widget.title != null ? Text(widget.title!) : Text('login').tr(),
),
body: FutureBuilder<String>(
future: _deviceIdFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('somethingWentWrong').tr());
}
final deviceId = snapshot.data!;
return Column(
children: [
Expanded(
child: InAppWebView(
initialSettings: InAppWebViewSettings(
userAgent:
kIsWeb
? null
: Platform.isIOS
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'
: Platform.isAndroid
? 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
),
initialUrlRequest: URLRequest(
url: WebUri('$serverUrl/auth/login/${widget.provider}'),
headers: {
if (token?.token.isNotEmpty ?? false)
'Authorization': 'AtField ${token!.token}',
'X-Device-Id': deviceId,
},
),
onWebViewCreated: (controller) {
// Register a handler to receive the token from JavaScript
controller.addJavaScriptHandler(
handlerName: 'tokenHandler',
callback: (args) {
// args[0] will be the token string
if (args.isNotEmpty && args[0] is String) {
setState(() {
authToken = args[0];
});
// Return the token and close the webview
Navigator.of(context).pop(authToken);
}
},
);
},
shouldOverrideUrlLoading: (
controller,
navigationAction,
) async {
final url = navigationAction.request.url;
if (url != null) {
setState(() {
currentUrl = url.toString();
_urlController.text = currentUrl ?? '';
_isLoading = true;
});
final path = url.path;
final queryParams = url.queryParameters;
// Check if we're on the token page
if (path.endsWith('/auth/callback')) {
// Extract token from URL
final challenge = queryParams['challenge'];
// Return the token and close the webview
Navigator.of(context).pop(challenge);
return NavigationActionPolicy.CANCEL;
}
}
return NavigationActionPolicy.ALLOW;
},
onUpdateVisitedHistory: (controller, url, androidIsReload) {
if (url != null) {
setState(() {
currentUrl = url.toString();
_urlController.text = currentUrl ?? '';
});
}
},
onLoadStop: (controller, url) {
setState(() {
_isLoading = false;
});
},
onLoadStart: (controller, url) {
setState(() {
_isLoading = true;
});
},
onLoadError: (controller, url, code, message) {
setState(() {
_isLoading = false;
});
},
),
),
// Loading progress indicator
if (_isLoading)
LinearProgressIndicator(
color: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.zero,
stopIndicatorRadius: 0,
minHeight: 2,
)
else
ColoredBox(
color: Theme.of(context).colorScheme.surfaceVariant,
).height(2),
// Debug location bar (only visible in debug mode)
Container(
padding: EdgeInsets.only(
left: 16,
right: 0,
bottom: MediaQuery.of(context).padding.bottom + 8,
top: 8,
),
color: Theme.of(context).colorScheme.surface,
child: Row(
children: [
Expanded(
child: TextField(
controller: _urlController,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
),
hintText: 'URL',
),
style: const TextStyle(fontSize: 12),
readOnly: true,
),
),
const Gap(4),
IconButton(
icon: const Icon(Icons.copy, size: 20),
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: () {
if (currentUrl != null) {
Clipboard.setData(ClipboardData(text: currentUrl!));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('copyToClipboard').tr(),
duration: const Duration(seconds: 1),
),
);
}
},
),
],
),
),
],
);
},
),
);
}
}

View File

@ -0,0 +1,86 @@
// ignore_for_file: invalid_runtime_check_with_js_interop_types
import 'dart:ui_web' as ui;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:web/web.dart' as web;
import 'package:flutter/material.dart';
class OidcScreen extends ConsumerStatefulWidget {
final String provider;
final String? title;
const OidcScreen({super.key, required this.provider, this.title});
@override
ConsumerState<OidcScreen> createState() => _OidcScreenState();
}
class _OidcScreenState extends ConsumerState<OidcScreen> {
bool _isInitialized = false;
final String _viewType = 'oidc-iframe';
void _setupWebListener(String serverUrl) {
// Listen for messages from the iframe
web.window.onMessage.listen((event) {
if (event.data != null && event.data is String) {
final message = event.data as String;
if (message.startsWith("token=")) {
String token = message.replaceFirst("token=", "");
// Return the token and close the screen
if (mounted) Navigator.pop(context, token);
}
}
});
// Create the iframe for the OIDC login
final token = ref.watch(tokenProvider);
final iframe =
web.HTMLIFrameElement()
..src =
(token?.token.isNotEmpty ?? false)
? '$serverUrl/auth/login/${widget.provider}?tk=${token!.token}'
: '$serverUrl/auth/login/${widget.provider}'
..style.border = 'none'
..width = '100%'
..height = '100%';
// Add the iframe to the document body
web.document.body!.append(iframe);
// Register the iframe as a platform view
ui.platformViewRegistry.registerViewFactory(
_viewType,
(int viewId) => iframe,
);
setState(() {
_isInitialized = true;
});
}
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
final serverUrl = ref.watch(serverUrlProvider);
_setupWebListener(serverUrl);
});
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: widget.title != null ? Text(widget.title!) : Text('login').tr(),
),
body:
_isInitialized
? HtmlElementView(viewType: _viewType)
: Center(child: CircularProgressIndicator()),
);
}
}

View File

@ -1,161 +0,0 @@
import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/symbols.dart';
final currentRouteProvider = StateProvider<String?>((ref) => null);
class TabNavigationObserver extends AutoRouterObserver {
Function(String?) onChange;
TabNavigationObserver({required this.onChange});
@override
void didPush(Route route, Route? previousRoute) {
log('pushed ${previousRoute?.settings.name} -> ${route.settings.name}');
if (route is DialogRoute) return;
Future(() {
onChange(route.settings.name);
});
}
@override
void didPop(Route route, Route? previousRoute) {
log('popped ${route.settings.name} -> ${previousRoute?.settings.name}');
if (route is DialogRoute) return;
Future(() {
onChange(previousRoute?.settings.name);
});
}
}
@RoutePage()
class TabsNavigationWidget extends HookConsumerWidget {
final Widget child;
final AppRouter router;
const TabsNavigationWidget({
super.key,
required this.child,
required this.router,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final useHorizontalLayout = isWideScreen(context);
final currentRoute = ref.watch(currentRouteProvider);
final notificationUnreadCount = ref.watch(
notificationUnreadCountNotifierProvider,
);
int activeIndex = 0;
final destinations = [
NavigationDestination(
label: 'explore'.tr(),
icon: const Icon(Symbols.explore),
),
NavigationDestination(label: 'chat'.tr(), icon: const Icon(Symbols.chat)),
NavigationDestination(
label: 'realms'.tr(),
icon: const Icon(Symbols.workspaces),
),
NavigationDestination(
label: 'account'.tr(),
icon: Badge.count(
count: notificationUnreadCount.value ?? 0,
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
child: const Icon(Symbols.account_circle),
),
),
];
final routes = <PageRouteInfo>[
ExploreRoute(),
ChatListRoute(),
RealmListRoute(),
AccountRoute(),
];
final routeNames = [
ExploreRoute.name,
ExploreShellRoute.name,
ChatListRoute.name,
RealmListRoute.name,
AccountRoute.name,
ChatShellRoute.name,
AccountShellRoute.name,
];
activeIndex = routes.indexWhere((route) => route.routeName == currentRoute);
if (activeIndex == -1) {
activeIndex = 0;
}
final isTabRoute = routeNames.any((route) {
return route == currentRoute;
});
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent,
body:
useHorizontalLayout
? Row(
children: [
ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
Gap(MediaQuery.of(context).padding.top + 8),
Expanded(
child: NavigationRail(
selectedIndex: activeIndex,
onDestinationSelected: (index) {
router.replace(routes[index]);
},
// labelType: NavigationRailLabelType.all,
destinations:
destinations
.map(
(d) => NavigationRailDestination(
icon: d.icon,
label: Text(d.label),
),
)
.toList(),
),
),
Gap(MediaQuery.of(context).padding.bottom + 8),
],
),
),
VerticalDivider(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
Expanded(child: child),
],
)
: child,
bottomNavigationBar:
!useHorizontalLayout && isTabRoute
? NavigationBar(
height: 56,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
selectedIndex: activeIndex,
onDestinationSelected: (index) {
router.replace(routes[index]);
},
destinations: destinations,
)
: null,
);
}
}

View File

@ -11,6 +11,7 @@ import 'package:island/widgets/chat/call_button.dart';
import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()
@ -32,37 +33,9 @@ class CallScreen extends HookConsumerWidget {
final viewMode = useState<String>('grid');
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: PageBackButton(
onWillPop: () {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
content: const Text(
'Do you want to leave the call or leave it in background?',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('In Background'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await callNotifier.disconnect();
callNotifier.dispose();
},
child: const Text('Leave'),
),
],
);
},
);
},
),
leading: PageBackButton(),
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -83,7 +56,7 @@ class CallScreen extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(Icons.grid_view),
icon: Icon(Symbols.grid_view),
tooltip: 'Grid View',
onPressed: () => viewMode.value = 'grid',
color:
@ -92,7 +65,7 @@ class CallScreen extends HookConsumerWidget {
: null,
),
IconButton(
icon: Icon(Icons.view_agenda),
icon: Icon(Symbols.view_agenda),
tooltip: 'Stage View',
onPressed: () => viewMode.value = 'stage',
color:

View File

@ -27,6 +27,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/realms/selection_dropdown.dart';
import 'package:island/widgets/response.dart';
import 'package:island/screens/tabs.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -241,6 +242,7 @@ class ChatListScreen extends HookConsumerWidget {
}
return AppScaffold(
extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar(
title: Text('chat').tr(),
bottom: TabBar(
@ -339,6 +341,7 @@ class ChatListScreen extends HookConsumerWidget {
},
child: const Icon(Symbols.add),
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: Stack(
children: [
Column(
@ -365,10 +368,10 @@ class ChatListScreen extends HookConsumerWidget {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding:
callState.isConnected
? EdgeInsets.only(bottom: 96)
: EdgeInsets.zero,
padding: getTabbedPadding(
context,
bottom: callState.isConnected ? 96 : null,
),
itemCount:
items
.where(
@ -428,7 +431,7 @@ class ChatListScreen extends HookConsumerWidget {
Positioned(
left: 0,
right: 0,
bottom: 0,
bottom: getTabbedPadding(context).bottom + 8,
child: const CallOverlayBar().padding(horizontal: 16, vertical: 12),
),
],

View File

@ -337,7 +337,7 @@ class ChatRoomScreen extends HookConsumerWidget {
),
);
typingDebouncer.value = Timer(const Duration(milliseconds: 1000), () {
typingDebouncer.value = Timer(const Duration(milliseconds: 850), () {
typingDebouncer.value = null;
});
}
@ -384,9 +384,12 @@ class ChatRoomScreen extends HookConsumerWidget {
if (!pkt.type.startsWith('messages')) return;
if (['messages.read'].contains(pkt.type)) return;
if (pkt.type == 'messages.typing') {
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'],
pkt.data?['sender'],
).copyWith(lastTyped: DateTime.now());
// Check if the sender is already in the typing list
@ -733,7 +736,7 @@ class ChatRoomScreen extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
duration: const Duration(milliseconds: 150),
switchInCurve: Curves.fastEaseInToSlowEaseOut,
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
transitionBuilder: (
@ -800,7 +803,7 @@ class ChatRoomScreen extends HookConsumerWidget {
),
)
: const SizedBox.shrink(
key: ValueKey('no_typing'),
key: ValueKey('typing-indicator-none'),
),
),
_ChatInput(

View File

@ -14,7 +14,7 @@ import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/paging_helper_ext.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@ -31,6 +31,206 @@ class ChatDetailScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final roomState = ref.watch(chatroomProvider(id));
final roomIdentity = ref.watch(chatroomIdentityProvider(id));
const kNotifyLevelText = [
'chatNotifyLevelAll',
'chatNotifyLevelMention',
'chatNotifyLevelNone',
];
void setNotifyLevel(int level) async {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/chat/$id/members/me/notify',
data: {'notify_level': level},
);
ref.invalidate(chatroomIdentityProvider(id));
if (context.mounted) {
showSnackBar(
context,
'chatNotifyLevelUpdated'.tr(args: [kNotifyLevelText[level].tr()]),
);
}
} catch (err) {
showErrorAlert(err);
}
}
void setChatBreak(DateTime until) async {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/chat/$id/members/me/notify',
data: {'break_until': until.toUtc().toIso8601String()},
);
ref.invalidate(chatroomProvider(id));
} catch (err) {
showErrorAlert(err);
}
}
void showNotifyLevelBottomSheet(SnChatMember identity) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => SheetScaffold(
height: 320,
titleText: 'chatNotifyLevel'.tr(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('chatNotifyLevelAll').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.notifications_active),
selected: identity.notify == 0,
onTap: () {
setNotifyLevel(0);
Navigator.pop(context);
},
),
ListTile(
title: const Text('chatNotifyLevelMention').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.alternate_email),
selected: identity.notify == 1,
onTap: () {
setNotifyLevel(1);
Navigator.pop(context);
},
),
ListTile(
title: const Text('chatNotifyLevelNone').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.notifications_off),
selected: identity.notify == 2,
onTap: () {
setNotifyLevel(2);
Navigator.pop(context);
},
),
],
),
),
);
}
void showChatBreakDialog() {
final now = DateTime.now();
final durationController = TextEditingController();
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('chatBreak').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('chatBreakDescription').tr(),
const Gap(16),
ListTile(
title: const Text('Clear').tr(),
subtitle: const Text('chatBreakClear').tr(),
leading: const Icon(Icons.notifications_active),
onTap: () {
setChatBreak(now);
Navigator.pop(context);
if (context.mounted) {
showSnackBar(context, 'chatBreakCleared'.tr());
}
},
),
ListTile(
title: const Text('5m'),
subtitle: const Text('chatBreakHour').tr(args: ['5m']),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 5)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar(context, 'chatBreakSet'.tr(args: ['5m']));
}
},
),
ListTile(
title: const Text('10m'),
subtitle: const Text('chatBreakHour').tr(args: ['10m']),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 10)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar(context, 'chatBreakSet'.tr(args: ['10m']));
}
},
),
ListTile(
title: const Text('15m'),
subtitle: const Text('chatBreakHour').tr(args: ['15m']),
leading: const Icon(Symbols.timer_3),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 15)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar(context, 'chatBreakSet'.tr(args: ['15m']));
}
},
),
ListTile(
title: const Text('30m'),
subtitle: const Text('chatBreakHour').tr(args: ['30m']),
leading: const Icon(Symbols.timer),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 30)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar(context, 'chatBreakSet'.tr(args: ['30m']));
}
},
),
const Gap(8),
TextField(
controller: durationController,
decoration: InputDecoration(
labelText: 'Custom (minutes)'.tr(),
hintText: 'Enter minutes'.tr(),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.check),
onPressed: () {
final minutes = int.tryParse(durationController.text);
if (minutes != null && minutes > 0) {
setChatBreak(now.add(Duration(minutes: minutes)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar(
context,
'chatBreakSet'.tr(args: ['${minutes}m']),
);
}
}
},
),
),
keyboardType: TextInputType.number,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('cancel').tr(),
),
],
),
);
}
const iconShadow = Shadow(
color: Colors.black54,
@ -114,17 +314,59 @@ class ChatDetailScreen extends HookConsumerWidget {
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentRoom.description ?? 'descriptionNone'.tr(),
style: const TextStyle(fontSize: 16),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentRoom.description ?? 'descriptionNone'.tr(),
style: const TextStyle(fontSize: 16),
).padding(all: 24),
const Divider(height: 1),
roomIdentity.when(
data:
(identity) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Symbols.notifications),
trailing: const Icon(Symbols.chevron_right),
title: const Text('chatNotifyLevel').tr(),
subtitle: Text(
kNotifyLevelText[identity!.notify].tr(),
),
onTap:
() =>
showNotifyLevelBottomSheet(identity),
),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.timer),
trailing: const Icon(Symbols.chevron_right),
title: const Text('chatBreak').tr(),
subtitle:
identity.breakUntil != null &&
identity.breakUntil!.isAfter(
DateTime.now(),
)
? Text(
DateFormat(
'yyyy-MM-dd HH:mm',
).format(identity.breakUntil!),
)
: const Text('chatBreakNone').tr(),
onTap: () => showChatBreakDialog(),
),
],
),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
],
),
),
],

View File

@ -263,6 +263,13 @@ class CreatorHubScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
context.router.push(
CreatorPostListRoute(
pubName: currentPublisher.value!.name,
),
);
},
),
Divider(height: 1).padding(vertical: 8),
ListTile(

View File

@ -0,0 +1,79 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
@RoutePage()
class CreatorPostListScreen extends HookConsumerWidget {
final String pubName;
const CreatorPostListScreen({
super.key,
@PathParam('name') required this.pubName,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final refreshKey = useState(0);
void showCreatePostSheet() {
showModalBottomSheet(
context: context,
builder:
(context) => SheetScaffold(
titleText: 'create'.tr(),
child: Column(
children: [
ListTile(
leading: const Icon(Symbols.edit),
title: Text('postContent'.tr()),
subtitle: Text('Create a regular post'),
onTap: () async {
Navigator.pop(context);
final result = await context.router.pushPath(
'/posts/compose?type=0',
);
if (result == true) {
refreshKey.value++;
}
},
),
ListTile(
leading: const Icon(Symbols.article),
title: Text('Article'),
subtitle: Text('Create a detailed article'),
onTap: () async {
Navigator.pop(context);
final result = await context.router.pushPath(
'/posts/compose?type=1',
);
if (result == true) {
refreshKey.value++;
}
},
),
],
),
),
);
}
return AppScaffold(
appBar: AppBar(title: Text('posts').tr()),
body: CustomScrollView(
key: ValueKey(refreshKey.value),
slivers: [
SliverPostList(pubName: pubName, itemType: PostItemType.creator),
],
),
floatingActionButton: FloatingActionButton(
onPressed: showCreatePostSheet,
child: const Icon(Symbols.add),
),
);
}
}

View File

@ -1,18 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/models/post.dart';
import 'package:island/widgets/check_in.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/tour/tour.dart';
import 'package:island/screens/tabs.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@ -47,7 +48,7 @@ class ExploreShellScreen extends ConsumerWidget {
}
@RoutePage()
class ExploreScreen extends ConsumerWidget {
class ExploreScreen extends HookConsumerWidget {
final bool isAside;
const ExploreScreen({super.key, this.isAside = false});
@ -58,11 +59,67 @@ class ExploreScreen extends ConsumerWidget {
return const EmptyPageHolder();
}
final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier);
final tabController = useTabController(initialLength: 3);
final currentFilter = useState<String?>(null);
useEffect(() {
void listener() {
switch (tabController.index) {
case 0:
currentFilter.value = null;
break;
case 1:
currentFilter.value = 'subscriptions';
break;
case 2:
currentFilter.value = 'friends';
break;
}
}
tabController.addListener(listener);
return () => tabController.removeListener(listener);
}, [tabController]);
final activitiesNotifier = ref.watch(
activityListNotifierProvider(currentFilter.value).notifier,
);
return TourTriggerWidget(
child: AppScaffold(
appBar: AppBar(title: const Text('explore').tr()),
extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar(
toolbarHeight: 0,
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'explore'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
@ -74,33 +131,50 @@ class ExploreScreen extends ConsumerWidget {
},
child: const Icon(Symbols.edit),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
body: RefreshIndicator(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
child: PagingHelperView(
provider: activityListNotifierProvider,
futureRefreshable: activityListNotifierProvider.future,
notifierRefreshable: activityListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => Center(
child: _ActivityListView(
data: data,
widgetCount: widgetCount,
endItemView: endItemView,
activitiesNotifier: activitiesNotifier,
),
),
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: TabBarView(
controller: tabController,
children: [
_buildActivityList(ref, null),
_buildActivityList(ref, 'subscriptions'),
_buildActivityList(ref, 'friends'),
],
),
),
);
}
Widget _buildActivityList(WidgetRef ref, String? filter) {
final activitiesNotifier = ref.watch(
activityListNotifierProvider(filter).notifier,
);
return RefreshIndicator(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
child: PagingHelperView(
provider: activityListNotifierProvider(filter),
futureRefreshable: activityListNotifierProvider(filter).future,
notifierRefreshable: activityListNotifierProvider(filter).notifier,
contentBuilder:
(data, widgetCount, endItemView) => Center(
child: _ActivityListView(
data: data,
widgetCount: widgetCount,
endItemView: endItemView,
activitiesNotifier: activitiesNotifier,
contentOnly: filter != null,
),
),
),
);
}
}
class _ActivityListView extends HookConsumerWidget {
final CursorPagingData<SnActivity> data;
final int widgetCount;
final Widget endItemView;
final bool contentOnly;
final ActivityListNotifier activitiesNotifier;
const _ActivityListView({
@ -108,6 +182,7 @@ class _ActivityListView extends HookConsumerWidget {
required this.widgetCount,
required this.endItemView,
required this.activitiesNotifier,
this.contentOnly = false,
});
@override
@ -116,7 +191,8 @@ class _ActivityListView extends HookConsumerWidget {
return CustomScrollView(
slivers: [
if (user.hasValue) SliverToBoxAdapter(child: CheckInWidget()),
if (user.hasValue && !contentOnly)
SliverToBoxAdapter(child: CheckInWidget()),
SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
@ -175,7 +251,7 @@ class _ActivityListView extends HookConsumerWidget {
return Column(children: [itemWidget, const Divider(height: 1)]);
},
),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
SliverGap(getTabbedPadding(context).bottom),
],
);
}
@ -185,19 +261,23 @@ class _ActivityListView extends HookConsumerWidget {
class ActivityListNotifier extends _$ActivityListNotifier
with CursorPagingNotifierMixin<SnActivity> {
@override
Future<CursorPagingData<SnActivity>> build() => fetch(cursor: null);
Future<CursorPagingData<SnActivity>> build(String? filter) =>
fetch(cursor: null);
@override
Future<CursorPagingData<SnActivity>> fetch({required String? cursor}) async {
final client = ref.read(apiClientProvider);
final take = 20;
final queryParameters = {
if (cursor != null) 'cursor': cursor,
'take': take,
if (filter != null) 'filter': filter,
};
final response = await client.get(
'/activities',
queryParameters: {
if (cursor != null) 'reading_cursor': cursor,
'take': take,
},
queryParameters: queryParameters,
);
final List<SnActivity> items =
@ -207,7 +287,12 @@ class ActivityListNotifier extends _$ActivityListNotifier
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
final nextCursor =
items.map((x) => x.createdAt).lastOrNull?.toIso8601String().toString();
items
.map((x) => x.createdAt)
.lastOrNull
?.toUtc()
.toIso8601String()
.toString();
return CursorPagingData(
items: items,

View File

@ -7,25 +7,174 @@ part of 'explore.dart';
// **************************************************************************
String _$activityListNotifierHash() =>
r'1baf0bb961bc02bfc8a5b5f515981072c6ce1750';
r'14ec2f211c86e1e64a9a34b142d0e8f78ff6361a';
/// 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 _$ActivityListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnActivity>> {
late final String? filter;
FutureOr<CursorPagingData<SnActivity>> build(String? filter);
}
/// See also [ActivityListNotifier].
@ProviderFor(ActivityListNotifier)
final activityListNotifierProvider = AutoDisposeAsyncNotifierProvider<
ActivityListNotifier,
CursorPagingData<SnActivity>
>.internal(
ActivityListNotifier.new,
name: r'activityListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$activityListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
const activityListNotifierProvider = ActivityListNotifierFamily();
/// See also [ActivityListNotifier].
class ActivityListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnActivity>>> {
/// See also [ActivityListNotifier].
const ActivityListNotifierFamily();
/// See also [ActivityListNotifier].
ActivityListNotifierProvider call(String? filter) {
return ActivityListNotifierProvider(filter);
}
@override
ActivityListNotifierProvider getProviderOverride(
covariant ActivityListNotifierProvider provider,
) {
return call(provider.filter);
}
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'activityListNotifierProvider';
}
/// See also [ActivityListNotifier].
class ActivityListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ActivityListNotifier,
CursorPagingData<SnActivity>
> {
/// See also [ActivityListNotifier].
ActivityListNotifierProvider(String? filter)
: this._internal(
() => ActivityListNotifier()..filter = filter,
from: activityListNotifierProvider,
name: r'activityListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$activityListNotifierHash,
dependencies: ActivityListNotifierFamily._dependencies,
allTransitiveDependencies:
ActivityListNotifierFamily._allTransitiveDependencies,
filter: filter,
);
ActivityListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.filter,
}) : super.internal();
final String? filter;
@override
FutureOr<CursorPagingData<SnActivity>> runNotifierBuild(
covariant ActivityListNotifier notifier,
) {
return notifier.build(filter);
}
@override
Override overrideWith(ActivityListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ActivityListNotifierProvider._internal(
() => create()..filter = filter,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
filter: filter,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ActivityListNotifier,
CursorPagingData<SnActivity>
>
createElement() {
return _ActivityListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ActivityListNotifierProvider && other.filter == filter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, filter.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ActivityListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnActivity>> {
/// The parameter `filter` of this provider.
String? get filter;
}
class _ActivityListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ActivityListNotifier,
CursorPagingData<SnActivity>
>
with ActivityListNotifierRef {
_ActivityListNotifierProviderElement(super.provider);
@override
String? get filter => (origin as ActivityListNotifierProvider).filter;
}
typedef _$ActivityListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnActivity>>;
// 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

View File

@ -47,8 +47,9 @@ class NotificationUnreadCountNotifier
void _subscribeToWebSocket() {
final webSocketService = ref.read(websocketProvider);
_subscription = webSocketService.dataStream.listen((packet) {
if (packet.type == 'notifications.new') {
_incrementCounter();
if (packet.type == 'notifications.new' && packet.data != null) {
final notification = SnNotification.fromJson(packet.data!);
if (notification.topic != 'messages.new') _incrementCounter();
}
});
}

View File

@ -7,7 +7,7 @@ part of 'notification.dart';
// **************************************************************************
String _$notificationUnreadCountNotifierHash() =>
r'372a2cc259d7d838cd4f33a9129f7396ef31dbb9';
r'0d5b07caa625c24575c5581d5fcd3089effc2844';
/// See also [NotificationUnreadCountNotifier].
@ProviderFor(NotificationUnreadCountNotifier)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,417 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/compose_settings_sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class ArticleEditScreen extends HookConsumerWidget {
final String id;
const ArticleEditScreen({super.key, @PathParam('id') required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final post = ref.watch(postProvider(id));
return post.when(
data: (post) => ArticleComposeScreen(originalPost: post),
loading:
() => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: const Center(child: CircularProgressIndicator()),
),
error:
(e, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Text('Error: $e', textAlign: TextAlign.center),
),
);
}
}
@RoutePage()
class ArticleComposeScreen extends HookConsumerWidget {
final SnPost? originalPost;
const ArticleComposeScreen({super.key, this.originalPost});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final publishers = ref.watch(publishersManagedProvider);
final state = useMemoized(
() => ComposeLogic.createState(originalPost: originalPost),
[originalPost],
);
final showPreview = useState(false);
// Initialize publisher once when data is available
useEffect(() {
if (publishers.value?.isNotEmpty ?? false) {
state.currentPublisher.value = publishers.value!.first;
}
return null;
}, [publishers]);
// Dispose state when widget is disposed
useEffect(() {
return () => ComposeLogic.dispose(state);
}, []);
// Helper methods
void showSettingsSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => ComposeSettingsSheet(
titleController: state.titleController,
descriptionController: state.descriptionController,
visibility: state.visibility,
onVisibilityChanged: () {
// Trigger rebuild if needed
},
),
);
}
void showKeyboardShortcutsDialog() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('keyboard_shortcuts'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'),
Text('Ctrl/Cmd + V: ${'paste'.tr()}'),
Text('Ctrl/Cmd + I: ${'add_image'.tr()}'),
Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'),
Text('Ctrl/Cmd + P: ${'toggle_preview'.tr()}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('close'.tr()),
),
],
),
);
}
Widget buildPreviewPane() {
return Container(
decoration: BoxDecoration(
border: Border.all(color: colorScheme.outline.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
Icon(Symbols.preview, size: 20),
const Gap(8),
Text('preview'.tr(), style: theme.textTheme.titleMedium),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.titleController.text.isNotEmpty) ...[
Text(
state.titleController.text,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Gap(16),
],
if (state.descriptionController.text.isNotEmpty) ...[
Text(
state.descriptionController.text,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface.withOpacity(0.7),
),
),
const Gap(16),
],
if (state.contentController.text.isNotEmpty)
Text(
state.contentController.text,
style: theme.textTheme.bodyMedium,
),
],
),
),
),
],
),
);
}
Widget buildEditorPane() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Publisher row
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id,
radius: 20,
fallbackIcon:
state.currentPublisher.value == null
? Symbols.question_mark
: null,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => const PublisherModal(),
).then((value) {
if (value != null) {
state.currentPublisher.value = value;
}
});
},
),
const Gap(12),
Text(
state.currentPublisher.value?.name ??
'postPublisherUnselected'.tr(),
style: theme.textTheme.bodyMedium,
),
],
),
),
),
// Content field with keyboard listener
Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey:
(event) => ComposeLogic.handleKeyPress(
event,
state,
ref,
context,
originalPost: originalPost,
postType: 1, // Article type
),
child: TextField(
controller: state.contentController,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
contentPadding: const EdgeInsets.all(8),
),
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
),
// Attachments preview
ValueListenableBuilder<List<UniversalFile>>(
valueListenable: state.attachments,
builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink();
return Column(
children: [
const Gap(16),
ValueListenableBuilder<Map<int, double>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var idx = 0; idx < attachments.length; idx++)
SizedBox(
width: 120,
height: 120,
child: AttachmentPreview(
item: attachments[idx],
progress: progressMap[idx],
onRequestUpload:
() => ComposeLogic.uploadAttachment(ref, state, idx),
onDelete:
() => ComposeLogic.deleteAttachment(ref, state, idx),
onMove: (delta) {
state.attachments.value = ComposeLogic.moveAttachment(
state.attachments.value,
idx,
delta,
);
},
),
),
],
);
},
),
],
);
},
),
],
);
}
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
actions: [
// Info banner for article compose
const SizedBox.shrink(),
IconButton(
icon: const Icon(Symbols.settings),
onPressed: showSettingsSheet,
tooltip: 'postSettings'.tr(),
),
Tooltip(
message: 'togglePreview'.tr(),
child: IconButton(
icon: Icon(showPreview.value ? Symbols.edit : Symbols.preview),
onPressed: () => showPreview.value = !showPreview.value,
),
),
if (isWideScreen(context))
Tooltip(
message: 'keyboard_shortcuts'.tr(),
child: IconButton(
icon: const Icon(Symbols.keyboard),
onPressed: showKeyboardShortcutsDialog,
),
),
ValueListenableBuilder<bool>(
valueListenable: state.submitting,
builder: (context, submitting, _) {
return IconButton(
onPressed:
submitting
? null
: () => ComposeLogic.performAction(
ref,
state,
context,
originalPost: originalPost,
postType: 1, // Article type
),
icon:
submitting
? SizedBox(
width: 28,
height: 28,
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
).center()
: Icon(
originalPost != null ? Symbols.edit : Symbols.upload,
),
);
},
),
const Gap(8),
],
),
body: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child:
isWideScreen(context)
? Row(
spacing: 16,
children: [
Expanded(
flex: showPreview.value ? 1 : 2,
child: buildEditorPane(),
),
if (showPreview.value)
Expanded(child: buildPreviewPane()),
],
)
: showPreview.value
? buildPreviewPane()
: buildEditorPane(),
),
),
// Bottom toolbar
Material(
elevation: 4,
child: Row(
children: [
IconButton(
onPressed: () => ComposeLogic.pickPhotoMedia(ref, state),
icon: const Icon(Symbols.add_a_photo),
color: colorScheme.primary,
),
IconButton(
onPressed: () => ComposeLogic.pickVideoMedia(ref, state),
icon: const Icon(Symbols.videocam),
color: colorScheme.primary,
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16,
top: 8,
),
),
],
),
);
}
}

View File

@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
@ -29,6 +30,7 @@ class PostDetailScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final post = ref.watch(postProvider(id));
final user = ref.watch(userInfoProvider);
final isWide = isWideScreen(context);
@ -47,6 +49,7 @@ class PostDetailScreen extends HookConsumerWidget {
PostItem(
item: post!,
isOpenable: false,
isFullPost: true,
backgroundColor: isWide ? Colors.transparent : null,
),
const Divider(height: 1),
@ -57,25 +60,25 @@ class PostDetailScreen extends HookConsumerWidget {
SliverGap(MediaQuery.of(context).padding.bottom + 80),
],
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Material(
elevation: 2,
color: Colors.transparent,
child: PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(id));
},
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
horizontal: 16,
if (user.value != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Material(
elevation: 2,
child: PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(id));
},
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
horizontal: 16,
),
),
),
),
],
);
},

View File

@ -7,13 +7,18 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/color.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@ -47,6 +52,21 @@ Future<SnSubscriptionStatus> publisherSubscriptionStatus(
return SnSubscriptionStatus.fromJson(resp.data);
}
@riverpod
Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
final publisher = await ref.watch(publisherProvider(pubName).future);
if (publisher.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: publisher.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
}
@RoutePage()
class PublisherProfileScreen extends HookConsumerWidget {
final String name;
@ -60,6 +80,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
final publisher = ref.watch(publisherProvider(name));
final badges = ref.watch(publisherBadgesProvider(name));
final subStatus = ref.watch(publisherSubscriptionStatusProvider(name));
final appbarColor = ref.watch(
publisherAppbarForcegroundColorProvider(name),
);
final subscribing = useState(false);
@ -91,8 +114,8 @@ class PublisherProfileScreen extends HookConsumerWidget {
}
}
final iconShadow = Shadow(
color: Colors.black54,
final appbarShadow = Shadow(
color: appbarColor.value?.invert ?? Colors.transparent,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
);
@ -103,69 +126,68 @@ class PublisherProfileScreen extends HookConsumerWidget {
body: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
data.background?.id != null
? CloudImageWidget(fileId: data.background!.id)
: Container(
color:
Theme.of(context).appBarTheme.backgroundColor,
),
title: Text(
data.nick,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
shadows: [iconShadow],
),
),
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
actions: [
subStatus.when(
data:
(status) => IconButton(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Icons.remove_circle
: Icons.add_circle,
shadows: [iconShadow],
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
width: 48,
height: 48,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.background?.id != null
? CloudImageWidget(file: data.background)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
const Gap(8),
],
),
background:
Container(), // Empty container since background is handled by Stack
),
],
),
),
SliverToBoxAdapter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
ProfilePictureWidget(
fileId: data.picture!.id,
radius: 32,
GestureDetector(
child: Badge(
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor:
Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(
file: data.picture,
radius: 32,
),
),
onTap: () {
Navigator.pop(context, true);
context.router.pushPath('/account/${data.name}');
},
),
Expanded(
child: Column(
@ -175,62 +197,115 @@ class PublisherProfileScreen extends HookConsumerWidget {
spacing: 6,
children: [
Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Text(
'@${data.name}',
).fontSize(14).opacity(0.85),
],
),
if (data.type == 0 && data.account != null)
InkWell(
onTap: () {
context.router.pushPath(
'/account/${data.account!.name}',
);
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 4,
children: [
Text(
'publisherVisitAccountPage'.tr(
args: ['@${data.account!.name}'],
),
).fontSize(14),
Icon(Icons.launch, size: 14),
],
).opacity(0.85),
).padding(bottom: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 6,
children: [
Icon(
data.type == 0
? Symbols.person
: Symbols.workspaces,
fill: 1,
size: 17,
),
Text(
'publisherBelongsTo'.tr(
args: ['@${data.account!.name}'],
),
).fontSize(14),
],
).opacity(0.85).padding(bottom: 6),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
padding: EdgeInsets.zero,
),
subStatus
.when(
data:
(status) => FilledButton.icon(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Symbols.remove_circle
: Symbols.add_circle,
),
label:
Text(
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(
vertical: -2,
),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
),
)
.padding(top: 8),
],
),
),
],
).padding(horizontal: 24, top: 24, bottom: 24),
).padding(horizontal: 24, top: 24),
),
SliverToBoxAdapter(
child: Column(
children: [
if (badges.value?.isNotEmpty ?? false)
BadgeList(badges: badges.value!).padding(top: 16),
if (data.verification != null)
VerificationStatusCard(
mark: data.verification!,
).padding(top: 16),
],
).padding(horizontal: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(vertical: 24),
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold(),
Text(
data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio,
),
],
).padding(horizontal: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 24),
),
if (badges.value?.isNotEmpty ?? false)
SliverToBoxAdapter(
child: BadgeList(
badges: badges.value!,
).padding(horizontal: 24, bottom: 24),
)
else
const SliverGap(16),
SliverToBoxAdapter(child: const Divider(height: 1)),
if (data.bio.isNotEmpty)
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [Text('bio').tr().bold(), Text(data.bio)],
).padding(horizontal: 24, top: 24),
),
if (data.bio.isNotEmpty)
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 24),
),
SliverPostList(pubName: name),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
],

View File

@ -399,5 +399,136 @@ class _PublisherSubscriptionStatusProviderElement
String get pubName => (origin as PublisherSubscriptionStatusProvider).pubName;
}
String _$publisherAppbarForcegroundColorHash() =>
r'3ff2eebb48d3f3af1907052f471e648f5b14b13c';
/// See also [publisherAppbarForcegroundColor].
@ProviderFor(publisherAppbarForcegroundColor)
const publisherAppbarForcegroundColorProvider =
PublisherAppbarForcegroundColorFamily();
/// See also [publisherAppbarForcegroundColor].
class PublisherAppbarForcegroundColorFamily extends Family<AsyncValue<Color?>> {
/// See also [publisherAppbarForcegroundColor].
const PublisherAppbarForcegroundColorFamily();
/// See also [publisherAppbarForcegroundColor].
PublisherAppbarForcegroundColorProvider call(String pubName) {
return PublisherAppbarForcegroundColorProvider(pubName);
}
@override
PublisherAppbarForcegroundColorProvider getProviderOverride(
covariant PublisherAppbarForcegroundColorProvider provider,
) {
return call(provider.pubName);
}
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'publisherAppbarForcegroundColorProvider';
}
/// See also [publisherAppbarForcegroundColor].
class PublisherAppbarForcegroundColorProvider
extends AutoDisposeFutureProvider<Color?> {
/// See also [publisherAppbarForcegroundColor].
PublisherAppbarForcegroundColorProvider(String pubName)
: this._internal(
(ref) => publisherAppbarForcegroundColor(
ref as PublisherAppbarForcegroundColorRef,
pubName,
),
from: publisherAppbarForcegroundColorProvider,
name: r'publisherAppbarForcegroundColorProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherAppbarForcegroundColorHash,
dependencies: PublisherAppbarForcegroundColorFamily._dependencies,
allTransitiveDependencies:
PublisherAppbarForcegroundColorFamily._allTransitiveDependencies,
pubName: pubName,
);
PublisherAppbarForcegroundColorProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
}) : super.internal();
final String pubName;
@override
Override overrideWith(
FutureOr<Color?> Function(PublisherAppbarForcegroundColorRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: PublisherAppbarForcegroundColorProvider._internal(
(ref) => create(ref as PublisherAppbarForcegroundColorRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
),
);
}
@override
AutoDisposeFutureProviderElement<Color?> createElement() {
return _PublisherAppbarForcegroundColorProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherAppbarForcegroundColorProvider &&
other.pubName == pubName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PublisherAppbarForcegroundColorRef
on AutoDisposeFutureProviderRef<Color?> {
/// The parameter `pubName` of this provider.
String get pubName;
}
class _PublisherAppbarForcegroundColorProviderElement
extends AutoDisposeFutureProviderElement<Color?>
with PublisherAppbarForcegroundColorRef {
_PublisherAppbarForcegroundColorProviderElement(super.provider);
@override
String get pubName =>
(origin as PublisherAppbarForcegroundColorProvider).pubName;
}
// 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

View File

@ -13,11 +13,13 @@ import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/file.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:island/screens/tabs.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@ -41,6 +43,7 @@ class RealmListScreen extends HookConsumerWidget {
final realmInvites = ref.watch(realmInvitesProvider);
return AppScaffold(
extendBody: false, // Prevent conflicts with tabs navigation
noBackground: false,
appBar: AppBar(
title: const Text('realms').tr(),
@ -83,6 +86,7 @@ class RealmListScreen extends HookConsumerWidget {
});
},
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: RefreshIndicator(
child: realms.when(
data:
@ -90,9 +94,7 @@ class RealmListScreen extends HookConsumerWidget {
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom,
),
padding: getTabbedPadding(context),
itemCount: value.length,
itemBuilder: (context, item) {
return ListTile(

View File

@ -16,6 +16,7 @@ import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/config.dart';
@ -232,7 +233,7 @@ class SettingsScreen extends HookConsumerWidget {
return ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundImageClear').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.texture),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
@ -248,6 +249,53 @@ class SettingsScreen extends HookConsumerWidget {
);
},
),
if (!kIsWeb && docBasepath.value != null)
FutureBuilder(
future:
File('${docBasepath.value}/$kAppBackgroundImagePath').exists(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink();
}
return ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundGenerateColor').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.format_color_fill),
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
showLoadingModal(context);
final palette = await PaletteGenerator.fromImageProvider(
FileImage(
File('${docBasepath.value}/$kAppBackgroundImagePath'),
),
);
if (palette.darkVibrantColor == null ||
palette.lightVibrantColor == null) {
if (context.mounted) hideLoadingModal(context);
showErrorAlert(
'Unable to calculate the domiant color of the background image.',
);
return;
}
if (!context.mounted) return;
final color =
MediaQuery.of(context).platformBrightness == Brightness.dark
? palette.darkVibrantColor!.color
: palette.lightVibrantColor!.color;
ref
.read(appSettingsNotifierProvider.notifier)
.setAppColorScheme(color.value);
if (context.mounted) {
hideLoadingModal(context);
showSnackBar(context, 'settingsApplied'.tr());
}
},
);
},
),
];
final serverSettings = [
@ -402,19 +450,24 @@ class SettingsScreen extends HookConsumerWidget {
children: [
_ShortcutRow(
shortcut: 'Ctrl+F',
description: 'settingsKeyboardShortcutSearch'.tr(),
description:
'settingsKeyboardShortcutSearch'.tr(),
),
_ShortcutRow(
shortcut: 'Ctrl+,',
description: 'settingsKeyboardShortcutSettings'.tr(),
description:
'settingsKeyboardShortcutSettings'.tr(),
),
_ShortcutRow(
shortcut: 'Ctrl+N',
description: 'settingsKeyboardShortcutNewMessage'.tr(),
description:
'settingsKeyboardShortcutNewMessage'.tr(),
),
_ShortcutRow(
shortcut: 'Esc',
description: 'settingsKeyboardShortcutCloseDialog'.tr(),
description:
'settingsKeyboardShortcutCloseDialog'
.tr(),
),
// Add more shortcuts as needed
],
@ -448,7 +501,10 @@ class SettingsScreen extends HookConsumerWidget {
title: 'settingsAppearance'.tr(),
children: appearanceSettings,
),
_SettingsSection(title: 'settingsServer'.tr(), children: serverSettings),
_SettingsSection(
title: 'settingsServer'.tr(),
children: serverSettings,
),
],
),
),
@ -475,11 +531,23 @@ class SettingsScreen extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(title: 'settingsAppearance'.tr(), children: appearanceSettings),
_SettingsSection(title: 'settingsServer'.tr(), children: serverSettings),
_SettingsSection(title: 'settingsBehavior'.tr(), children: behaviorSettings),
_SettingsSection(
title: 'settingsAppearance'.tr(),
children: appearanceSettings,
),
_SettingsSection(
title: 'settingsServer'.tr(),
children: serverSettings,
),
_SettingsSection(
title: 'settingsBehavior'.tr(),
children: behaviorSettings,
),
if (desktopSettings.isNotEmpty)
_SettingsSection(title: 'settingsDesktop'.tr(), children: desktopSettings),
_SettingsSection(
title: 'settingsDesktop'.tr(),
children: desktopSettings,
),
],
);
}

152
lib/screens/tabs.dart Normal file
View File

@ -0,0 +1,152 @@
import 'dart:ui';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/symbols.dart';
@RoutePage()
class TabsScreen extends HookConsumerWidget {
const TabsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final useHorizontalLayout = isWideScreen(context);
final notificationUnreadCount = ref.watch(
notificationUnreadCountNotifierProvider,
);
final destinations = [
NavigationDestination(
label: 'explore'.tr(),
icon: const Icon(Symbols.explore),
),
NavigationDestination(label: 'chat'.tr(), icon: const Icon(Symbols.chat)),
NavigationDestination(
label: 'realms'.tr(),
icon: const Icon(Symbols.workspaces),
),
NavigationDestination(
label: 'account'.tr(),
icon: Badge.count(
count: notificationUnreadCount.value ?? 0,
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
child: const Icon(Symbols.account_circle),
),
),
];
final routes = <PageRouteInfo>[
ExploreRoute(),
ChatListRoute(),
RealmListRoute(),
AccountRoute(),
];
return AutoTabsRouter.tabBar(
routes: routes,
scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
builder: (context, child, _) {
final tabsRouter = AutoTabsRouter.of(context);
if (isWideScreen(context)) {
return Row(
children: [
NavigationRail(
destinations:
destinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
),
)
.toList(),
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: tabsRouter.setActiveIndex,
),
const VerticalDivider(width: 1),
Expanded(child: child),
],
);
}
return Stack(
children: [
Positioned.fill(child: child),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surface.withOpacity(0.8),
),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: NavigationBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
overlayColor: WidgetStatePropertyAll(
Colors.transparent,
),
surfaceTintColor: Colors.transparent,
height: 56,
labelBehavior:
NavigationDestinationLabelBehavior.alwaysHide,
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: tabsRouter.setActiveIndex,
destinations: destinations,
),
),
),
),
),
),
],
);
},
);
}
}
class TabbedFabLocation extends FloatingActionButtonLocation {
final BuildContext context;
const TabbedFabLocation(this.context);
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final mediaQuery = MediaQuery.of(context);
final safeAreaPadding = mediaQuery.padding;
// Calculate position with proper safe area considerations
final double fabX =
scaffoldGeometry.scaffoldSize.width -
scaffoldGeometry.floatingActionButtonSize.width -
16.0 -
safeAreaPadding.right;
// Use safe area bottom padding + navigation bar height (typically 80px)
final double fabY =
scaffoldGeometry.scaffoldSize.height -
scaffoldGeometry.floatingActionButtonSize.height -
scaffoldGeometry.bottomSheetSize.height -
safeAreaPadding.bottom -
80.0 +
16;
return Offset(fabX, fabY);
}
}

7
lib/services/color.dart Normal file
View File

@ -0,0 +1,7 @@
import 'package:flutter/widgets.dart';
extension ColorInversion on Color {
Color get invert {
return Color.fromARGB(alpha, 255 - red, 255 - green, 255 - blue);
}
}

View File

@ -8,6 +8,7 @@ import 'package:cross_file/cross_file.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:island/models/file.dart';
import 'package:native_exif/native_exif.dart';
import 'package:tus_client_dart/tus_client_dart.dart';
Future<XFile?> cropImage(
@ -46,7 +47,91 @@ Completer<SnCloudFile?> putMediaToCloud({
String? mimetype,
Function(double progress, Duration estimate)? onProgress,
}) {
XFile file;
final completer = Completer<SnCloudFile?>();
// Process the image to remove GPS EXIF data if needed
if (fileData.isOnDevice && fileData.type == UniversalFileType.image) {
final data = fileData.data;
if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
// Use native_exif to selectively remove GPS data
Exif.fromPath(data.path)
.then((exif) {
// Remove GPS-related attributes
final gpsAttributes = [
'GPSLatitude',
'GPSLatitudeRef',
'GPSLongitude',
'GPSLongitudeRef',
'GPSAltitude',
'GPSAltitudeRef',
'GPSTimeStamp',
'GPSProcessingMethod',
'GPSDateStamp',
];
// Create a map of attributes to clear
final clearAttributes = <String, String>{};
for (final attr in gpsAttributes) {
clearAttributes[attr] = '';
}
// Write empty values to remove GPS data
return exif.writeAttributes(clearAttributes);
})
.then((_) {
// Continue with upload after GPS data is removed
_processUpload(
fileData,
atk,
baseUrl,
filename,
mimetype,
onProgress,
completer,
);
})
.catchError((e) {
// If there's an error, continue with the original file
debugPrint('Error removing GPS EXIF data: $e');
_processUpload(
fileData,
atk,
baseUrl,
filename,
mimetype,
onProgress,
completer,
);
});
return completer;
}
}
// If not an image or on web, continue with normal upload
_processUpload(
fileData,
atk,
baseUrl,
filename,
mimetype,
onProgress,
completer,
);
return completer;
}
// Helper method to process the upload after any EXIF processing
Completer<SnCloudFile?> _processUpload(
UniversalFile fileData,
String atk,
String baseUrl,
String? filename,
String? mimetype,
Function(double progress, Duration estimate)? onProgress,
Completer<SnCloudFile?> completer,
) {
late XFile file;
String actualFilename = filename ?? 'randomly_file';
String actualMimetype = mimetype ?? '';
Uint8List? byteData;
@ -63,16 +148,23 @@ Completer<SnCloudFile?> putMediaToCloud({
actualFilename = filename ?? 'uploaded_file';
actualMimetype = mimetype ?? 'application/octet-stream';
if (mimetype == null) {
throw ArgumentError('Mimetype is required when providing raw bytes.');
completer.completeError(
ArgumentError('Mimetype is required when providing raw bytes.'),
);
return completer;
}
file = XFile.fromData(byteData!, mimeType: actualMimetype);
} else if (data is SnCloudFile) {
// If the file is already on the cloud, just return it
return Completer<SnCloudFile?>()..complete(data);
completer.complete(data);
return completer;
} else {
throw ArgumentError(
'Invalid fileData type. Expected data to be XFile, List<int>, Uint8List, or SnCloudFile.',
completer.completeError(
ArgumentError(
'Invalid fileData type. Expected data to be XFile, List<int>, Uint8List, or SnCloudFile.',
),
);
return completer;
}
final Map<String, String> metadata = {
@ -80,8 +172,6 @@ Completer<SnCloudFile?> putMediaToCloud({
'content-type': actualMimetype,
};
final completer = Completer<SnCloudFile?>();
final client = TusClient(file);
client
.upload(

View File

@ -15,3 +15,32 @@ bool isWiderScreen(BuildContext context) {
bool isWidestScreen(BuildContext context) {
return MediaQuery.of(context).size.width > kWidescreenWidth;
}
EdgeInsets getTabbedPadding(
BuildContext context, {
double? horizontal,
double? vertical,
double? left,
double? right,
double? top,
double? bottom,
}) {
if (isWideScreen(context)) {
return EdgeInsets.only(
left: left ?? horizontal ?? 0,
right: right ?? horizontal ?? 0,
top: top ?? vertical ?? 0,
bottom: bottom ?? vertical ?? 0,
);
}
final effectiveBottom = bottom ?? vertical;
return EdgeInsets.only(
left: left ?? horizontal ?? 0,
right: right ?? horizontal ?? 0,
top: top ?? vertical ?? 0,
bottom:
effectiveBottom != null
? effectiveBottom + MediaQuery.of(context).padding.bottom + 16
: MediaQuery.of(context).padding.bottom + 16,
);
}

14
lib/services/text.dart Normal file
View File

@ -0,0 +1,14 @@
extension StringExtension on String {
String capitalizeEachWord() {
if (isEmpty) return this;
return split(' ')
.map(
(word) =>
word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}'
: '',
)
.join(' ');
}
}

76
lib/services/time.dart Normal file
View File

@ -0,0 +1,76 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart';
import 'package:relative_time/relative_time.dart';
extension DurationFormatter on Duration {
String formatDuration() {
final isNegative = inMicroseconds < 0;
final positiveDuration = isNegative ? -this : this;
final hours = positiveDuration.inHours.toString().padLeft(2, '0');
final minutes = (positiveDuration.inMinutes % 60).toString().padLeft(
2,
'0',
);
final seconds = (positiveDuration.inSeconds % 60).toString().padLeft(
2,
'0',
);
return '${isNegative ? '-' : ''}$hours:$minutes:$seconds';
}
String formatOffset() {
final isNegative = inMicroseconds < 0;
final positiveDuration = isNegative ? -this : this;
final hours = positiveDuration.inHours.toString().padLeft(2, '0');
final minutes = (positiveDuration.inMinutes % 60).toString().padLeft(
2,
'0',
);
return '${isNegative ? '-' : '+'}$hours:$minutes';
}
String formatOffsetLocal() {
// Get the local timezone offset
final localOffset = DateTime.now().timeZoneOffset;
// Add the local offset to the input duration
final totalOffset = this - localOffset;
final isNegative = totalOffset.inMicroseconds < 0;
final positiveDuration = isNegative ? -totalOffset : totalOffset;
final hours = positiveDuration.inHours.toString().padLeft(2, '0');
final minutes = (positiveDuration.inMinutes % 60).toString().padLeft(
2,
'0',
);
return '${isNegative ? '-' : '+'}$hours:$minutes';
}
}
extension DateTimeFormatter on DateTime {
String formatSystem() {
return DateFormat.yMd().add_jm().format(toLocal());
}
String formatCustom(String pattern) {
return DateFormat(pattern).format(toLocal());
}
String formatCustomGlobal(String pattern) {
return DateFormat(pattern).format(this);
}
String formatWithLocale(String locale) {
return DateFormat.yMd().add_jm().format(toLocal()).toString();
}
String formatRelative(BuildContext context) {
return RelativeTime(context).format(toLocal());
}
}

View File

@ -0,0 +1 @@
export 'timezone/native.dart' if (dart.library.html) 'timezone/web.dart';

View File

@ -0,0 +1,22 @@
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/standalone.dart' as tz;
import 'package:timezone/data/latest_all.dart' as tzdb;
Future<void> initializeTzdb() async {
tzdb.initializeTimeZones();
}
(Duration offset, DateTime now) getTzInfo(String name) {
final location = tz.getLocation(name);
final now = tz.TZDateTime.now(location);
final offset = now.timeZoneOffset;
return (offset, now);
}
Future<String> getMachineTz() async {
return await FlutterTimezone.getLocalTimezone();
}
List<String> getAvailableTz() {
return tz.timeZoneDatabase.locations.keys.toList();
}

View File

@ -0,0 +1,21 @@
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/browser.dart' as tz;
Future<void> initializeTzdb() async {
await tz.initializeTimeZone();
}
(Duration offset, DateTime now) getTzInfo(String name) {
final location = tz.getLocation(name);
final now = tz.TZDateTime.now(location);
final offset = now.timeZoneOffset;
return (offset, now);
}
Future<String> getMachineTz() async {
return await FlutterTimezone.getLocalTimezone();
}
List<String> getAvailableTz() {
return tz.timeZoneDatabase.locations.keys.toList();
}

View File

@ -0,0 +1,99 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/user.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
const kVerificationMarkColors = [
Colors.teal,
Colors.blue,
Colors.amber,
Colors.blueGrey,
Colors.lightBlue,
];
class AccountName extends StatelessWidget {
final SnAccount account;
final TextStyle? style;
const AccountName({super.key, required this.account, this.style});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(child: Text(account.nick, style: style)),
if (account.profile.verification != null)
VerificationMark(mark: account.profile.verification!),
],
);
}
}
class VerificationMark extends StatelessWidget {
final SnVerificationMark mark;
const VerificationMark({super.key, required this.mark});
@override
Widget build(BuildContext context) {
return Tooltip(
richMessage: TextSpan(
text: mark.title ?? 'No title',
children: [
TextSpan(text: '\n'),
TextSpan(
text: mark.description ?? 'descriptionNone'.tr(),
style: TextStyle(fontWeight: FontWeight.normal),
),
],
style: TextStyle(fontWeight: FontWeight.bold),
),
child: Icon(
mark.type == 4
? Symbols.play_circle
: mark.type == 0
? Symbols.build_circle
: Symbols.verified,
size: 16,
color: kVerificationMarkColors[mark.type],
fill: 1,
),
);
}
}
class VerificationStatusCard extends StatelessWidget {
final SnVerificationMark mark;
const VerificationStatusCard({super.key, required this.mark});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
mark.type == 4
? Symbols.play_circle
: mark.type == 0
? Symbols.build_circle
: Symbols.verified,
size: 32,
color: kVerificationMarkColors[mark.type],
fill: 1,
),
const Gap(8),
Text(mark.title ?? 'No title').bold(),
Text(mark.description ?? 'descriptionNone'.tr()),
const Gap(6),
Text(
'Verified by\n${mark.verifiedBy ?? 'No one verified it'}',
).fontSize(11).opacity(0.8),
],
).padding(horizontal: 24, vertical: 16),
);
}
}

View File

@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:easy_localization/easy_localization.dart';
class AccountNameplate extends HookConsumerWidget {
final String name;
final bool isOutlined;
final EdgeInsetsGeometry padding;
const AccountNameplate({
super.key,
required this.name,
this.isOutlined = true,
this.padding = const EdgeInsets.all(16),
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(accountProvider(name));
return Container(
decoration:
isOutlined
? BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
)
: null,
margin: padding,
child: Card(
margin: EdgeInsets.zero,
elevation: 0,
color: Colors.transparent,
child: user.when(
data:
(account) =>
account.profile.background != null
? AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
// Background image
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudFileWidget(
item: account.profile.background!,
fit: BoxFit.cover,
),
),
),
// Gradient overlay for text readability
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.1),
Colors.transparent,
],
),
),
),
),
// Content positioned at the bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'@${account.name}',
).textColor(Colors.white70),
],
),
),
],
),
),
),
],
),
)
: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
decoration:
isOutlined
? BoxDecoration(
border: Border.all(
color:
Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(12),
)
: null,
child: Row(
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
Text('@${account.name}'),
],
),
),
],
),
),
loading:
() => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Loading indicator (equivalent to leading)
const CircularProgressIndicator(),
const SizedBox(width: 16),
// Loading text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('loading').bold().tr(),
const SizedBox(height: 4),
const Text('...'),
],
),
),
],
),
),
error:
(error, stackTrace) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Error icon (equivalent to leading)
const Icon(Symbols.error),
const SizedBox(width: 16),
// Error text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('somethingWentWrong').tr().bold(),
const SizedBox(height: 4),
Text(error.toString()),
],
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,169 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/services/time.dart';
import 'package:island/services/timezone/native.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class AccountProfileCard extends HookConsumerWidget {
final String uname;
const AccountProfileCard({super.key, required this.uname});
@override
Widget build(BuildContext context, WidgetRef ref) {
final account = ref.watch(accountProvider(uname));
final width =
math.max(MediaQuery.of(context).size.width - 80, 360).toDouble();
return PopupCard(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
child: SizedBox(
width: width,
child: account.when(
data:
(data) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (data.profile.background != null)
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: CloudImageWidget(file: data.profile.background),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
ProfilePictureWidget(file: data.profile.picture),
const Gap(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountName(
account: data,
style: TextStyle(fontWeight: FontWeight.bold),
),
Text('@${data.name}').fontSize(12),
],
),
),
],
),
const Gap(12),
AccountStatusWidget(
uname: data.name,
padding: EdgeInsets.zero,
),
if (data.profile.timeZone.isNotEmpty)
Row(
spacing: 6,
children: [
Icon(
Symbols.alarm,
size: 17,
fill: 1,
).padding(right: 2),
Text(
getTzInfo(
data.profile.timeZone,
).$2.formatCustomGlobal('HH:mm'),
).fontSize(12),
Text(
getTzInfo(
data.profile.timeZone,
).$1.formatOffsetLocal(),
).fontSize(12),
],
).padding(top: 2),
if (data.badges.isNotEmpty)
BadgeList(badges: data.badges).padding(top: 12),
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
).padding(top: 12),
FilledButton.tonalIcon(
onPressed: () {
Navigator.pop(context);
context.router.pushPath('/account/${data.name}');
},
icon: const Icon(Symbols.launch),
label: Text('accountProfileView').tr(),
).padding(top: 12, horizontal: 2),
],
).padding(horizontal: 24, vertical: 16),
],
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(accountProvider(uname)),
),
loading:
() => SizedBox(
width: width,
height: width,
child:
Padding(
padding: const EdgeInsets.all(24),
child: CircularProgressIndicator(),
).center(),
),
),
),
);
}
}
class AccountPfcGestureDetector extends StatelessWidget {
final String uname;
final Widget child;
const AccountPfcGestureDetector({
super.key,
required this.uname,
required this.child,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
child: child,
onTapDown: (details) {
showAccountProfileCard(context, uname, offset: details.localPosition);
},
);
}
}
Future<void> showAccountProfileCard(
BuildContext context,
String uname, {
Offset? offset,
}) async {
await showPopupCard<void>(
offset: offset ?? Offset.zero,
context: context,
builder: (context) => AccountProfileCard(uname: uname),
dimBackground: true,
);
}

View File

@ -0,0 +1,193 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:table_calendar/table_calendar.dart';
/// A reusable widget for displaying an event calendar with event details
/// This can be used in various places throughout the app
class EventCalendarWidget extends HookConsumerWidget {
/// The list of calendar entries to display
final AsyncValue<List<SnEventCalendarEntry>> events;
/// Initial date to focus on
final DateTime? initialDate;
/// Whether to show the event details below the calendar
final bool showEventDetails;
/// Whether to constrain the width of the calendar
final bool constrainWidth;
/// Maximum width constraint when constrainWidth is true
final double maxWidth;
/// Callback when a day is selected
final void Function(DateTime)? onDaySelected;
/// Callback when the focused month changes
final void Function(int year, int month)? onMonthChanged;
const EventCalendarWidget({
super.key,
required this.events,
this.initialDate,
this.showEventDetails = true,
this.constrainWidth = false,
this.maxWidth = 480,
this.onDaySelected,
this.onMonthChanged,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedMonth = useState(initialDate?.month ?? DateTime.now().month);
final selectedYear = useState(initialDate?.year ?? DateTime.now().year);
final selectedDay = useState(initialDate ?? DateTime.now());
final content = Column(
children: [
TableCalendar(
locale: EasyLocalization.of(context)!.locale.toString(),
firstDay: DateTime.now().add(Duration(days: -3650)),
lastDay: DateTime.now().add(Duration(days: 3650)),
focusedDay: DateTime.utc(
selectedYear.value,
selectedMonth.value,
selectedDay.value.day,
),
weekNumbersVisible: false,
calendarFormat: CalendarFormat.month,
selectedDayPredicate: (day) {
return isSameDay(selectedDay.value, day);
},
onDaySelected: (value, _) {
selectedDay.value = value;
onDaySelected?.call(value);
},
onPageChanged: (focusedDay) {
selectedMonth.value = focusedDay.month;
selectedYear.value = focusedDay.year;
onMonthChanged?.call(focusedDay.year, focusedDay.month);
},
eventLoader: (day) {
return events.value
?.where((e) => isSameDay(e.date, day))
.expand((e) => [...e.statuses, e.checkInResult])
.where((e) => e != null)
.toList() ??
[];
},
calendarBuilders: CalendarBuilders(
dowBuilder: (context, day) {
final text = DateFormat.EEEEE().format(day);
return Center(child: Text(text));
},
markerBuilder: (context, day, events) {
var checkInResult =
events.whereType<SnCheckInResult>().firstOrNull;
if (checkInResult != null) {
return Positioned(
top: 32,
child: Text(
'checkInResultT${checkInResult.level}'.tr(),
style: TextStyle(
fontSize: 9,
color:
isSameDay(selectedDay.value, day)
? Theme.of(context).colorScheme.onPrimaryContainer
: isSameDay(DateTime.now(), day)
? Theme.of(
context,
).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
return null;
},
),
),
if (showEventDetails) ...[
const Divider(height: 1).padding(top: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Builder(
builder: (context) {
final event =
events.value
?.where((e) => isSameDay(e.date, selectedDay.value))
.firstOrNull;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(DateFormat.EEEE().format(selectedDay.value))
.fontSize(16)
.bold()
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Text(DateFormat.yMd().format(selectedDay.value))
.fontSize(12)
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
const Gap(16),
if (event?.checkInResult != null)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'checkInResultLevel${event!.checkInResult!.level}',
).tr().fontSize(16).bold(),
for (final tip in event.checkInResult!.tips)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Icon(
Symbols.circle,
size: 12,
fill: 1,
).padding(top: 4, right: 4),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(tip.title).bold(),
Text(tip.content),
],
),
),
],
).padding(top: 8),
],
),
if (event?.checkInResult == null &&
(event?.statuses.isEmpty ?? true))
Text('eventCalanderEmpty').tr(),
],
).padding(vertical: 24, horizontal: 24);
},
),
),
],
],
);
if (constrainWidth) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Card(margin: EdgeInsets.all(16), child: content),
).center();
}
return content;
}
}

View File

@ -0,0 +1,280 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:styled_widget/styled_widget.dart';
/// A widget that displays a graph of fortune levels over time
/// This can be used alongside the EventCalendarWidget to provide a different visualization
class FortuneGraphWidget extends HookConsumerWidget {
/// The list of calendar entries to display
final AsyncValue<List<SnEventCalendarEntry>> events;
/// Whether to constrain the width of the graph
final bool constrainWidth;
/// Maximum width constraint when constrainWidth is true
final double maxWidth;
/// Height of the graph
final double height;
/// Callback when a point is selected
final void Function(DateTime)? onPointSelected;
final String? eventCalanderUser;
const FortuneGraphWidget({
super.key,
required this.events,
this.constrainWidth = false,
this.maxWidth = double.infinity,
this.height = 180,
this.onPointSelected,
this.eventCalanderUser,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Filter events to only include those with check-in results
final filteredEvents = events.whenData(
(data) =>
data
.where((event) => event.checkInResult != null)
.toList()
.cast<SnEventCalendarEntry>()
// Sort by date
..sort((a, b) => a.date.compareTo(b.date)),
);
final content = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('fortuneGraph').tr().fontSize(18).bold(),
if (eventCalanderUser != null)
IconButton(
icon: const Icon(Icons.calendar_month, size: 20),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
context.router.pushNamed(
'/account/$eventCalanderUser/calendar',
);
},
),
],
).padding(all: 16, bottom: 24),
SizedBox(
height: height,
child: filteredEvents.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noFortuneData').tr());
}
// Create spots for the line chart
final spots =
data
.map(
(e) => FlSpot(
e.date.millisecondsSinceEpoch.toDouble(),
e.checkInResult!.level.toDouble(),
),
)
.toList();
// Get min and max dates for the x-axis
final minDate = data.first.date;
final maxDate = data.last.date;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
horizontalInterval: 1,
drawVerticalLine: false,
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: _calculateDateInterval(minDate, maxDate),
getTitlesWidget: (value, meta) {
final date = DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
DateFormat.MMMd().format(date),
style: TextStyle(fontSize: 10),
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
reservedSize: 40,
getTitlesWidget: (value, meta) {
final level = value.toInt();
if (level < 0 || level > 4) return const SizedBox();
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
'checkInResultT$level'.tr(),
style: TextStyle(fontSize: 10),
),
);
},
),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
),
left: BorderSide(color: Theme.of(context).dividerColor),
),
),
minX: minDate.millisecondsSinceEpoch.toDouble(),
maxX: maxDate.millisecondsSinceEpoch.toDouble(),
minY: 0,
maxY: 4,
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (touchedSpots) {
return touchedSpots.map((spot) {
final date = DateTime.fromMillisecondsSinceEpoch(
spot.x.toInt(),
);
final level = spot.y.toInt();
return LineTooltipItem(
'${DateFormat.yMMMd().format(date)}\n',
TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: 'checkInResultLevel$level'.tr(),
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.normal,
),
),
],
);
}).toList();
},
),
touchCallback: (
FlTouchEvent event,
LineTouchResponse? response,
) {
if (event is FlTapUpEvent &&
response != null &&
response.lineBarSpots != null &&
response.lineBarSpots!.isNotEmpty) {
final spot = response.lineBarSpots!.first;
final date = DateTime.fromMillisecondsSinceEpoch(
spot.x.toInt(),
);
onPointSelected?.call(date);
}
},
),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: Theme.of(context).colorScheme.primary,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Theme.of(context).colorScheme.primary,
strokeWidth: 2,
strokeColor:
Theme.of(context).colorScheme.surface,
);
},
),
belowBarData: BarAreaData(
show: true,
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.2),
),
),
],
),
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
),
],
);
if (constrainWidth) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Card(margin: EdgeInsets.all(16), child: content),
).center();
}
return content;
}
/// Calculate an appropriate interval for date labels based on the date range
double _calculateDateInterval(DateTime minDate, DateTime maxDate) {
final difference = maxDate.difference(minDate).inDays;
// If less than 7 days, show all days
if (difference <= 7) {
return 24 * 60 * 60 * 1000; // One day in milliseconds
}
// If less than a month, show every 3 days
if (difference <= 30) {
return 3 * 24 * 60 * 60 * 1000; // Three days in milliseconds
}
// If less than 3 months, show weekly
if (difference <= 90) {
return 7 * 24 * 60 * 60 * 1000; // One week in milliseconds
}
// Otherwise show every 2 weeks
return 14 * 24 * 60 * 60 * 1000; // Two weeks in milliseconds
}
}

View File

@ -19,10 +19,10 @@ class LevelingProgressCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('levelingProgress').tr().fontSize(16).bold(),
Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.baseline,

View File

@ -1,15 +1,13 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/account/status_creation.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@ -108,14 +106,15 @@ class AccountStatusWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userStatus = ref.watch(accountStatusProvider(uname));
final status = ref.watch(accountStatusProvider(uname));
final account = ref.watch(accountProvider(uname));
return Padding(
padding: padding ?? EdgeInsets.symmetric(horizontal: 27, vertical: 4),
child: Row(
spacing: 4,
children: [
if (userStatus.value?.isOnline ?? false)
if (status.value?.isOnline ?? false)
Icon(
Symbols.circle,
fill: 1,
@ -123,13 +122,24 @@ class AccountStatusWidget extends HookConsumerWidget {
size: 16,
).padding(right: 4)
else
Icon(Symbols.circle, color: Colors.grey, size: 16).padding(all: 4),
if (userStatus.value?.isCustomized ?? false)
Text(userStatus.value?.label ?? 'unknown'.tr())
Icon(
Symbols.circle,
color: Colors.grey,
size: 16,
).padding(right: 4),
if (status.value?.isCustomized ?? false)
Text(status.value?.label ?? 'unknown'.tr())
else
Text((userStatus.value?.label ?? 'offline').toLowerCase()).tr(),
Text((status.value?.label ?? 'offline').toLowerCase()).tr(),
if (!(status.value?.isOnline ?? false) &&
account.value?.profile.lastSeenAt != null)
Flexible(
child: Text(
account.value!.profile.lastSeenAt!.formatRelative(context),
).opacity(0.75),
),
],
),
).opacity((userStatus.value?.isCustomized ?? false) ? 1 : 0.85);
).opacity((status.value?.isCustomized ?? false) ? 1 : 0.85);
}
}

View File

@ -0,0 +1,462 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/main.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'app_notification.freezed.dart';
part 'app_notification.g.dart';
class AppNotificationToast extends HookConsumerWidget {
const AppNotificationToast({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifications = ref.watch(appNotificationsProvider);
// Create a global key for AnimatedList
final listKey = useMemoized(() => GlobalKey<AnimatedListState>());
// Track visual notification count (including those being animated out)
final visualCount = useState(notifications.length);
// Track notifications being removed to manage visual count
final animatingOutIds = useState<Set<String>>({});
// Track previous notifications to detect changes
final previousNotifications = usePrevious(notifications) ?? [];
// Handle notification changes
useEffect(() {
final currentIds = notifications.map((n) => n.data.id).toSet();
final previousIds = previousNotifications.map((n) => n.data.id).toSet();
// Find new notifications (added)
final newIds = currentIds.difference(previousIds);
// Update visual count for new notifications
if (newIds.isNotEmpty) {
visualCount.value += newIds.length;
}
// Insert new notifications with animation
for (final id in newIds) {
final index = notifications.indexWhere((n) => n.data.id == id);
if (index != -1 &&
listKey.currentState != null &&
index >= 0 &&
index <= notifications.length) {
try {
listKey.currentState!.insertItem(
index,
duration: const Duration(milliseconds: 150),
);
} catch (e) {
// Log error but don't crash the app
debugPrint('Error inserting notification: $e');
}
}
}
return null;
}, [notifications]);
return Positioned(
top: MediaQuery.of(context).padding.top + 50,
left: 16,
right: 16,
child: SizedBox(
// Use visualCount instead of notifications.length for height calculation
height: visualCount.value * 80,
child: AnimatedList(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
key: listKey,
initialItemCount: notifications.length,
itemBuilder: (context, index, animation) {
// Safely access notifications with bounds check
if (index >= notifications.length) {
return const SizedBox.shrink(); // Return empty widget if out of bounds
}
final notification = notifications[index];
final now = DateTime.now();
final createdAt = notification.createdAt ?? now;
final duration =
notification.duration ?? const Duration(seconds: 5);
final elapsedTime = now.difference(createdAt);
final remainingTime = duration - elapsedTime;
final progress =
1.0 -
(remainingTime.inMilliseconds / duration.inMilliseconds).clamp(
0.0,
1.0,
); // Ensure progress is clamped
return SizeTransition(
sizeFactor: animation.drive(
CurveTween(curve: Curves.fastLinearToSlowEaseIn),
),
child: _NotificationCard(
notification: notification,
progress: progress.clamp(0.0, 1.0),
onDismiss: () {
// Find the current index before removal
final currentIndex = notifications.indexWhere(
(n) => n.data.id == notification.data.id,
);
// Add to animating out set
final notificationId = notification.data.id;
if (!animatingOutIds.value.contains(notificationId)) {
animatingOutIds.value = {
...animatingOutIds.value,
notificationId,
};
}
if (currentIndex != -1 &&
listKey.currentState != null &&
currentIndex >= 0 &&
currentIndex < notifications.length) {
try {
// Remove the item with animation
listKey.currentState!.removeItem(
currentIndex,
(context, animation) => SizeTransition(
sizeFactor: animation.drive(
CurveTween(curve: Curves.fastLinearToSlowEaseIn),
),
child: _NotificationCard(
notification: notification,
progress: progress.clamp(0.0, 1.0),
onDismiss:
() {}, // Empty because it's being removed
),
),
duration: const Duration(milliseconds: 150),
// When animation completes, update the visual count
);
// Schedule decrementing the visual count after animation completes
Future.delayed(const Duration(milliseconds: 150), () {
if (animatingOutIds.value.contains(notificationId)) {
visualCount.value =
visualCount.value > 0 ? visualCount.value - 1 : 0;
animatingOutIds.value =
animatingOutIds.value
.where((id) => id != notificationId)
.toSet();
}
});
} catch (e) {
// Log error but don't crash the app
log('[Notification] Error removing notification: $e');
// Still update visual count in case of error
visualCount.value =
visualCount.value > 0 ? visualCount.value - 1 : 0;
animatingOutIds.value =
animatingOutIds.value
.where((id) => id != notificationId)
.toSet();
}
}
// Actually remove from state
ref
.read(appNotificationsProvider.notifier)
.removeNotification(notification);
},
),
);
},
),
),
);
}
}
class _NotificationCard extends HookConsumerWidget {
final AppNotification notification;
final double progress;
final VoidCallback onDismiss;
const _NotificationCard({
required this.notification,
required this.progress,
required this.onDismiss,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Use state to track the current progress for smooth animation
final progressState = useState(progress);
// Use effect to update progress smoothly
useEffect(() {
if (progress < 1.0) {
// Update progress every 16ms (roughly 60fps) for smooth animation
final timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
final now = DateTime.now();
final createdAt = notification.createdAt ?? now;
final duration = notification.duration ?? const Duration(seconds: 5);
final elapsedTime = now.difference(createdAt);
final remainingTime = duration - elapsedTime;
final newProgress = (1.0 -
(remainingTime.inMilliseconds / duration.inMilliseconds))
.clamp(0.0, 1.0);
progressState.value = newProgress;
// Auto-dismiss when complete
if (newProgress >= 1.0) {
onDismiss();
}
});
return timer.cancel;
}
return null;
}, [notification.createdAt, notification.duration]);
return Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
if (notification.data.meta['action_uri'] != null) {
var uri = notification.data.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
appRouter.pushPath(notification.data.meta['action_uri']);
} else {
// External URLs
launchUrlString(uri);
}
onDismiss();
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Progress indicator
if (progressState.value > 0 && progressState.value < 1.0)
AnimatedBuilder(
animation: progressState,
builder: (context, _) {
return LinearProgressIndicator(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
),
value: 1.0 - progressState.value,
backgroundColor: Colors.transparent,
color: Theme.of(context).colorScheme.tertiary,
minHeight: 3,
stopIndicatorColor: Colors.transparent,
stopIndicatorRadius: 0,
);
},
),
Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (notification.data.meta['avatar'] != null)
ProfilePictureWidget(
fileId: notification.data.meta['avatar'],
radius: 12,
).padding(right: 12, top: 2)
else if (notification.icon != null)
Icon(
notification.icon,
color: Theme.of(context).colorScheme.primary,
size: 24,
).padding(right: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
notification.data.title,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
if (notification.data.content.isNotEmpty)
Text(
notification.data.content,
style: Theme.of(context).textTheme.bodyMedium,
),
if (notification.data.subtitle.isNotEmpty)
Text(
notification.data.subtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
IconButton(
icon: const Icon(Symbols.close, size: 18),
onPressed: onDismiss,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
),
],
),
),
);
}
}
@freezed
sealed class AppNotification with _$AppNotification {
const factory AppNotification({
required SnNotification data,
@JsonKey(ignore: true) IconData? icon,
@JsonKey(ignore: true) Duration? duration,
@Default(null) DateTime? createdAt,
@Default(false) @JsonKey(ignore: true) bool isAnimatingOut,
}) = _AppNotification;
factory AppNotification.fromJson(Map<String, dynamic> json) =>
_$AppNotificationFromJson(json);
}
// Using riverpod_generator for cleaner provider code
@riverpod
class AppNotifications extends _$AppNotifications {
StreamSubscription? _subscription;
@override
List<AppNotification> build() {
ref.onDispose(() {
_subscription?.cancel();
});
_initWebSocketListener();
return [];
}
void _initWebSocketListener() {
final service = ref.read(websocketProvider);
_subscription = service.dataStream.listen((packet) {
// Handle notification packets
if (packet.type == 'notifications.new') {
try {
final data = SnNotification.fromJson(packet.data!);
IconData? icon;
switch (data.topic) {
case 'general':
default:
icon = Symbols.info;
break;
}
addNotification(
AppNotification(
data: data,
icon: icon,
createdAt: data.createdAt.toLocal(),
duration: const Duration(seconds: 5),
),
);
} catch (e) {
log('[Notification] Error processing notification: $e');
}
}
});
}
void addNotification(AppNotification notification) {
// Create a new notification with createdAt if not provided
final newNotification =
notification.createdAt == null
? notification.copyWith(createdAt: DateTime.now())
: notification;
// Add to state
state = [...state, newNotification];
// Auto-remove notification after duration
final duration = newNotification.duration ?? const Duration(seconds: 5);
Future.delayed(duration, () {
// Find the notification in the current state
final notificationToRemove = state.firstWhereOrNull(
(n) => n.data.id == newNotification.data.id,
);
// Only proceed if the notification still exists in state
if (notificationToRemove != null) {
// Call removeNotification which will handle the animation
removeNotification(notificationToRemove);
}
});
}
// Map to track notifications that are being animated out
final Map<String, bool> _animatingNotifications = {};
// Map to track which notifications should animate out
final Map<String, bool> _animatingOutNotifications = {};
void removeNotification(AppNotification notification) {
final notificationId = notification.data.id;
// If this notification is already being removed, don't do anything
if (_animatingNotifications[notificationId] == true) {
return;
}
// Mark this notification as being removed
_animatingNotifications[notificationId] = true;
// Remove from state immediately - AnimatedList handles the animation
state = state.where((n) => n.data.id != notificationId).toList();
// Clean up tracking
_animatingNotifications.remove(notificationId);
_animatingOutNotifications.remove(notificationId);
}
// Helper method to check if a notification should animate out
bool isAnimatingOut(String notificationId) {
return _animatingOutNotifications[notificationId] == true;
}
// Helper method to manually add a notification for testing
void showNotification({
required SnNotification data,
IconData? icon,
Duration? duration,
}) {
addNotification(
AppNotification(
data: data,
icon: icon,
duration: duration,
createdAt: data.createdAt,
),
);
}
}

View File

@ -0,0 +1,190 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'app_notification.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppNotification implements DiagnosticableTreeMixin {
SnNotification get data;@JsonKey(ignore: true) IconData? get icon;@JsonKey(ignore: true) Duration? get duration; DateTime? get createdAt;@JsonKey(ignore: true) bool get isAnimatingOut;
/// Create a copy of AppNotification
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AppNotificationCopyWith<AppNotification> get copyWith => _$AppNotificationCopyWithImpl<AppNotification>(this as AppNotification, _$identity);
/// Serializes this AppNotification to a JSON map.
Map<String, dynamic> toJson();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'AppNotification'))
..add(DiagnosticsProperty('data', data))..add(DiagnosticsProperty('icon', icon))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('createdAt', createdAt))..add(DiagnosticsProperty('isAnimatingOut', isAnimatingOut));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppNotification&&(identical(other.data, data) || other.data == data)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isAnimatingOut, isAnimatingOut) || other.isAnimatingOut == isAnimatingOut));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt,isAnimatingOut);
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt, isAnimatingOut: $isAnimatingOut)';
}
}
/// @nodoc
abstract mixin class $AppNotificationCopyWith<$Res> {
factory $AppNotificationCopyWith(AppNotification value, $Res Function(AppNotification) _then) = _$AppNotificationCopyWithImpl;
@useResult
$Res call({
SnNotification data,@JsonKey(ignore: true) IconData? icon,@JsonKey(ignore: true) Duration? duration, DateTime? createdAt,@JsonKey(ignore: true) bool isAnimatingOut
});
$SnNotificationCopyWith<$Res> get data;
}
/// @nodoc
class _$AppNotificationCopyWithImpl<$Res>
implements $AppNotificationCopyWith<$Res> {
_$AppNotificationCopyWithImpl(this._self, this._then);
final AppNotification _self;
final $Res Function(AppNotification) _then;
/// Create a copy of AppNotification
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? data = null,Object? icon = freezed,Object? duration = freezed,Object? createdAt = freezed,Object? isAnimatingOut = null,}) {
return _then(_self.copyWith(
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as SnNotification,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as IconData?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isAnimatingOut: null == isAnimatingOut ? _self.isAnimatingOut : isAnimatingOut // ignore: cast_nullable_to_non_nullable
as bool,
));
}
/// Create a copy of AppNotification
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnNotificationCopyWith<$Res> get data {
return $SnNotificationCopyWith<$Res>(_self.data, (value) {
return _then(_self.copyWith(data: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _AppNotification with DiagnosticableTreeMixin implements AppNotification {
const _AppNotification({required this.data, @JsonKey(ignore: true) this.icon, @JsonKey(ignore: true) this.duration, this.createdAt = null, @JsonKey(ignore: true) this.isAnimatingOut = false});
factory _AppNotification.fromJson(Map<String, dynamic> json) => _$AppNotificationFromJson(json);
@override final SnNotification data;
@override@JsonKey(ignore: true) final IconData? icon;
@override@JsonKey(ignore: true) final Duration? duration;
@override@JsonKey() final DateTime? createdAt;
@override@JsonKey(ignore: true) final bool isAnimatingOut;
/// Create a copy of AppNotification
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AppNotificationCopyWith<_AppNotification> get copyWith => __$AppNotificationCopyWithImpl<_AppNotification>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AppNotificationToJson(this, );
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'AppNotification'))
..add(DiagnosticsProperty('data', data))..add(DiagnosticsProperty('icon', icon))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('createdAt', createdAt))..add(DiagnosticsProperty('isAnimatingOut', isAnimatingOut));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppNotification&&(identical(other.data, data) || other.data == data)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isAnimatingOut, isAnimatingOut) || other.isAnimatingOut == isAnimatingOut));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt,isAnimatingOut);
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt, isAnimatingOut: $isAnimatingOut)';
}
}
/// @nodoc
abstract mixin class _$AppNotificationCopyWith<$Res> implements $AppNotificationCopyWith<$Res> {
factory _$AppNotificationCopyWith(_AppNotification value, $Res Function(_AppNotification) _then) = __$AppNotificationCopyWithImpl;
@override @useResult
$Res call({
SnNotification data,@JsonKey(ignore: true) IconData? icon,@JsonKey(ignore: true) Duration? duration, DateTime? createdAt,@JsonKey(ignore: true) bool isAnimatingOut
});
@override $SnNotificationCopyWith<$Res> get data;
}
/// @nodoc
class __$AppNotificationCopyWithImpl<$Res>
implements _$AppNotificationCopyWith<$Res> {
__$AppNotificationCopyWithImpl(this._self, this._then);
final _AppNotification _self;
final $Res Function(_AppNotification) _then;
/// Create a copy of AppNotification
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? data = null,Object? icon = freezed,Object? duration = freezed,Object? createdAt = freezed,Object? isAnimatingOut = null,}) {
return _then(_AppNotification(
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as SnNotification,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as IconData?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isAnimatingOut: null == isAnimatingOut ? _self.isAnimatingOut : isAnimatingOut // ignore: cast_nullable_to_non_nullable
as bool,
));
}
/// Create a copy of AppNotification
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnNotificationCopyWith<$Res> get data {
return $SnNotificationCopyWith<$Res>(_self.data, (value) {
return _then(_self.copyWith(data: value));
});
}
}
// dart format on

View File

@ -0,0 +1,48 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_notification.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_AppNotification _$AppNotificationFromJson(Map<String, dynamic> json) =>
_AppNotification(
data: SnNotification.fromJson(json['data'] as Map<String, dynamic>),
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
);
Map<String, dynamic> _$AppNotificationToJson(_AppNotification instance) =>
<String, dynamic>{
'data': instance.data.toJson(),
'created_at': instance.createdAt?.toIso8601String(),
};
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$appNotificationsHash() => r'a7e7e1d1533e329b000d4b294e455b8420ec3c4d';
/// See also [AppNotifications].
@ProviderFor(AppNotifications)
final appNotificationsProvider = AutoDisposeNotifierProvider<
AppNotifications,
List<AppNotification>
>.internal(
AppNotifications.new,
name: r'appNotificationsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$appNotificationsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppNotifications = AutoDisposeNotifier<List<AppNotification>>;
// 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

View File

@ -10,6 +10,7 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_notification.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart';
@ -83,6 +84,7 @@ class WindowScaffold extends HookConsumerWidget {
],
),
_WebSocketIndicator(),
AppNotificationToast(),
],
),
);
@ -90,7 +92,11 @@ class WindowScaffold extends HookConsumerWidget {
return Stack(
fit: StackFit.expand,
children: [child, _WebSocketIndicator()],
children: [
Positioned.fill(child: child),
_WebSocketIndicator(),
AppNotificationToast(),
],
);
}
}
@ -110,6 +116,7 @@ class AppScaffold extends StatelessWidget {
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
final bool? noBackground;
final bool? extendBody;
const AppScaffold({
super.key,
@ -125,6 +132,7 @@ class AppScaffold extends StatelessWidget {
this.onDrawerChanged,
this.onEndDrawerChanged,
this.noBackground,
this.extendBody,
});
@override
@ -144,7 +152,7 @@ class AppScaffold extends StatelessWidget {
);
return Scaffold(
extendBody: true,
extendBody: extendBody ?? true,
extendBodyBehindAppBar: true,
backgroundColor:
noBackground
@ -167,9 +175,10 @@ class AppScaffold extends StatelessWidget {
}
class PageBackButton extends StatelessWidget {
final Color? color;
final List<Shadow>? shadows;
final VoidCallback? onWillPop;
const PageBackButton({super.key, this.shadows, this.onWillPop});
const PageBackButton({super.key, this.shadows, this.onWillPop, this.color});
@override
Widget build(BuildContext context) {
@ -179,6 +188,7 @@ class PageBackButton extends StatelessWidget {
context.router.maybePop();
},
icon: Icon(
color: color,
(!kIsWeb && (Platform.isMacOS || Platform.isIOS))
? Symbols.arrow_back_ios_new
: Symbols.arrow_back,

View File

@ -4,10 +4,11 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:livekit_client/livekit_client.dart';
class CallControlsBar extends HookConsumerWidget {
const CallControlsBar({super.key});
@ -17,76 +18,227 @@ class CallControlsBar extends HookConsumerWidget {
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
final userInfo = ref.watch(userInfoProvider);
final actionButtonStyle = ButtonStyle(
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
);
return Card(
margin: const EdgeInsets.only(left: 12, right: 12, top: 8),
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Row(
children: [
Builder(
builder: (context) {
if (callNotifier.localParticipant == null) {
return CircularProgressIndicator().center();
}
return SizedBox(
width: 40,
height: 40,
child:
SpeakingRippleAvatar(
isSpeaking:
callNotifier.localParticipant!.isSpeaking,
audioLevel:
callNotifier.localParticipant!.audioLevel,
pictureId: userInfo.value?.profile.picture?.id,
size: 36,
).center(),
);
},
),
],
),
_buildCircularButtonWithDropdown(
context: context,
ref: ref,
icon:
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
onPressed: () => callNotifier.toggleCamera(),
backgroundColor: const Color(0xFF424242),
hasDropdown: true,
deviceType: 'videoinput',
),
IconButton(
icon: Icon(
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
),
onPressed: () {
callNotifier.toggleMicrophone();
},
style: actionButtonStyle,
const Gap(16),
_buildCircularButton(
icon:
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
onPressed: () => callNotifier.toggleScreenShare(),
backgroundColor: const Color(0xFF424242),
),
IconButton(
icon: Icon(
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
),
onPressed: () {
callNotifier.toggleCamera();
},
style: actionButtonStyle,
const Gap(16),
_buildCircularButtonWithDropdown(
context: context,
ref: ref,
icon: callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
onPressed: () => callNotifier.toggleMicrophone(),
backgroundColor: const Color(0xFF424242),
hasDropdown: true,
deviceType: 'audioinput',
),
IconButton(
icon: Icon(
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
),
onPressed: () {
callNotifier.toggleScreenShare();
},
style: actionButtonStyle,
const Gap(16),
_buildCircularButton(
icon: Icons.call_end,
onPressed: () => callNotifier.disconnect(),
backgroundColor: const Color(0xFFE53E3E),
iconColor: Colors.white,
),
],
).padding(all: 16),
),
);
}
Widget _buildCircularButton({
required IconData icon,
required VoidCallback onPressed,
required Color backgroundColor,
Color? iconColor,
}) {
return Container(
width: 56,
height: 56,
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
child: IconButton(
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
onPressed: onPressed,
),
);
}
Widget _buildCircularButtonWithDropdown({
required BuildContext context,
required WidgetRef ref,
required IconData icon,
required VoidCallback onPressed,
required Color backgroundColor,
required bool hasDropdown,
Color? iconColor,
String? deviceType, // 'videoinput' or 'audioinput'
}) {
return Stack(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
onPressed: onPressed,
),
),
if (hasDropdown && deviceType != null)
Positioned(
bottom: 4,
right: 4,
child: GestureDetector(
onTap: () => _showDeviceSelectionDialog(context, ref, deviceType),
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: backgroundColor.withOpacity(0.8),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 0.5,
),
),
child: Icon(
Icons.arrow_drop_down,
color: Colors.white,
size: 12,
),
),
),
),
],
);
}
Future<void> _showDeviceSelectionDialog(
BuildContext context,
WidgetRef ref,
String deviceType,
) async {
try {
final devices = await Hardware.instance.enumerateDevices(
type: deviceType,
);
if (!context.mounted) return;
showModalBottomSheet(
context: context,
builder: (BuildContext dialogContext) {
return SheetScaffold(
titleText:
deviceType == 'videoinput'
? 'selectCamera'.tr()
: 'selectMicrophone'.tr(),
child: ListView.builder(
itemCount: devices.length,
itemBuilder: (context, index) {
final device = devices[index];
return ListTile(
title: Text(
device.label.isNotEmpty
? device.label
: '${'device'.tr()} ${index + 1}',
),
onTap: () {
Navigator.of(dialogContext).pop();
_switchDevice(context, ref, device, deviceType);
},
);
},
),
);
},
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${'failedToEnumerateDevices'.tr()}: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _switchDevice(
BuildContext context,
WidgetRef ref,
MediaDevice device,
String deviceType,
) async {
try {
final callNotifier = ref.read(callNotifierProvider.notifier);
if (deviceType == 'videoinput') {
// Switch camera device
final localParticipant = callNotifier.room?.localParticipant;
final videoTrack =
localParticipant?.videoTrackPublications.firstOrNull?.track;
if (videoTrack is LocalVideoTrack) {
await videoTrack.switchCamera(device.deviceId);
}
} else if (deviceType == 'audioinput') {
// Switch microphone device
final localParticipant = callNotifier.room?.localParticipant;
final audioTrack =
localParticipant?.audioTrackPublications.firstOrNull?.track;
if (audioTrack is LocalAudioTrack) {
// For audio devices, we need to restart the track with new device
await audioTrack.restartTrack(
AudioCaptureOptions(deviceId: device.deviceId),
);
}
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${'switchedTo'.tr()} ${device.label.isNotEmpty ? device.label : 'selectedDevice'.tr()}',
),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${'failedToSwitchDevice'.tr()}: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
class CallOverlayBar extends HookConsumerWidget {

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
@ -8,11 +9,14 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/embed.dart';
import 'package:island/pods/call.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart';
@ -129,9 +133,12 @@ class MessageItem extends HookConsumerWidget {
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: [
ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 16,
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 16,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -223,6 +230,20 @@ class MessageItem extends HookConsumerWidget {
).padding(vertical: 4);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.where((embed) => embed['Type'] == 'link')
.map((embed) => SnEmbedLink.fromJson(embed as Map<String, dynamic>))
.map((link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(constraints.maxWidth, 480),
margin: const EdgeInsets.symmetric(vertical: 4),
);
},
))
.toList()),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@ -10,13 +10,13 @@ import 'package:gal/gal.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:path/path.dart' show extension;
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:uuid/uuid.dart';
import 'package:dio/dio.dart';
class CloudFileList extends HookConsumerWidget {
final List<SnCloudFile> files;
@ -194,14 +194,18 @@ class CloudFileZoomIn extends HookConsumerWidget {
);
// Get the image URL
final imageUrl = '$serverUrl/files/${item.id}?original=true';
final client = ref.watch(apiClientProvider);
// Create a temporary file to save the image
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}';
await Dio().download(imageUrl, filePath);
await Gal.putImage(filePath);
await client.download(
'/files/${item.id}',
filePath,
queryParameters: {'original': true},
);
await Gal.putImage(filePath, album: 'Solar Network');
// Show success message
scaffold.showSnackBar(

Some files were not shown because too many files have changed in this diff Show More