Compare commits

..

21 Commits

Author SHA1 Message Date
0bbfa6ddde 🚀 Launch 3.0.0+104 2025-06-09 01:10:43 +08:00
1f46f89056 👽 Support new activity API 2025-06-09 01:06:52 +08:00
7740cf7830 🐛 Fixes members' loading bugs 2025-06-08 22:33:47 +08:00
d165e488ad 💄 Optimized image loading and error 2025-06-08 20:31:32 +08:00
37f5c61905 🐛 Fixes on video caching policy 2025-06-08 20:22:46 +08:00
723e17ff47 Account contact settings 2025-06-08 20:02:57 +08:00
20e6cc4283 Typing indicator 2025-06-08 01:16:48 +08:00
b2a118bbd0 🐛 Fixes on enter to send 2025-06-07 23:31:02 +08:00
3dd7c8a5b2 Account settings auth devices 2025-06-07 22:56:25 +08:00
d832729278 💄 Optimized the auth experience 2025-06-07 12:14:57 +08:00
dff8532229 💄 Better otp input 2025-06-07 02:50:39 +08:00
bf77bfce64 Auth factor settings 2025-06-06 01:21:22 +08:00
accb99bb24 Merge pull request #22 from Texas0295/v3 2025-06-05 18:54:03 +08:00
e67429e513 [FIX] fix: 🐛 fix workflow file icon location error 2025-06-05 13:16:15 +08:00
a87f5d61e6 [FIX] fix: 🐛 fix appimage build error 2025-06-05 12:51:20 +08:00
b84fafb53c 🚀 Launch 3.0.0+103 2025-06-04 02:03:33 +08:00
9e9d8b6fab 🐛 Bug fixes, and fixes 2025-06-04 01:59:17 +08:00
088cb4d5a2 🚀 Launch 3.0.0+102 2025-06-02 22:52:19 +08:00
33e84805d7 :drunk: I have no idea what did I did 2025-06-02 22:51:52 +08:00
9aca6eb674 Post show replies / forwareded 2025-06-02 02:43:57 +08:00
e431a54a89 🐛 Fix NSE, get token from flutter side in native iOS 2025-06-02 02:23:59 +08:00
77 changed files with 4983 additions and 1322 deletions

83
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,83 @@
name: Build Release
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
build-web:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build web --release
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-web
path: build/web
build-exe:
runs-on: windows-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build windows
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-windows
path: build/windows/x64/runner/Release
build-linux:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install -y libmpv-dev mpv
sudo apt-get install -y libayatana-appindicator3-dev
sudo apt-get install -y keybinder-3.0
sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux
path: build/linux/x64/release/bundle
- name: Build AppImage
run: |
rm -r Solian.AppDir | true
mkdir Solian.AppDir
cp -r build/linux/x64/release/bundle/* Solian.AppDir
cp -r buildtools/appimage_config/* Solian.AppDir
cp assets/icons/icon-padded.png Solian.AppDir
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
sudo chmod +x Solian.AppDir/AppRun
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'

View File

@ -99,6 +99,16 @@
"permissionMember": "Member", "permissionMember": "Member",
"reply": "Reply", "reply": "Reply",
"forward": "Forward", "forward": "Forward",
"repliedTo": "Replied to",
"forwarded": "Forwarded",
"hasAttachments": {
"one": "{} attachment",
"other": "{} attachments"
},
"postHasAttachments": {
"one": "{} attachment",
"other": "{} attachments"
},
"edited": "Edited", "edited": "Edited",
"addVideo": "Add video", "addVideo": "Add video",
"addPhoto": "Add photo", "addPhoto": "Add photo",
@ -155,7 +165,7 @@
"status": "Status", "status": "Status",
"statusActivityTitle": "{} is {} {}", "statusActivityTitle": "{} is {} {}",
"statusActivityEndedTitle": "{} is {} {} until {}", "statusActivityEndedTitle": "{} is {} {} until {}",
"appSettings": "App settings", "appSettings": "App Settings",
"accountSettings": "Account Settings", "accountSettings": "Account Settings",
"settings": "Settings", "settings": "Settings",
"language": "Language", "language": "Language",
@ -292,28 +302,99 @@
"accountDeletionHint": "Are you sure to delete your account? If you confirmed, we will send an confirmation email to your primary email address, you can continue the deletion process by follow the insturctions in the email.", "accountDeletionHint": "Are you sure to delete your account? If you confirmed, we will send an confirmation email to your primary email address, you can continue the deletion process by follow the insturctions in the email.",
"accountDeletionSent": "Account deletion confirmation email sent, please check your email inbox.", "accountDeletionSent": "Account deletion confirmation email sent, please check your email inbox.",
"accountSecurityTitle": "Security", "accountSecurityTitle": "Security",
"accountPrivacyTitle": "Privacy",
"accountDangerZoneTitle": "Danger Zone", "accountDangerZoneTitle": "Danger Zone",
"accountPassword": "Password", "accountPassword": "Password",
"accountPasswordDescription": "Change your account password", "accountPasswordDescription": "Change your account password",
"accountPasswordChange": "Change Password", "accountPasswordChange": "Change Password",
"accountPasswordChangeSent": "Password reset link sent, please check your email inbox.", "accountPasswordChangeSent": "Password reset link sent, please check your email inbox.",
"accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.", "accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.",
"accountTwoFactor": "Two-Factor Authentication", "accountAuthFactor": "Auth factors",
"accountTwoFactorDescription": "Add an extra layer of security to your account", "accountAuthFactorDescription": "Multi-factor authentication to ensure safety and convience",
"accountTwoFactorSetup": "Set Up 2FA",
"accountTwoFactorSetupDescription": "Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to sign in.",
"accountPrivacy": "Privacy Settings",
"accountPrivacyDescription": "Control who can see your profile and content",
"accountDataExport": "Export Your Data",
"accountDataExportDescription": "Download a copy of your data",
"accountDataExportConfirmation": "We'll prepare an export of your data which may take some time. You'll receive an email when it's ready to download.",
"accountDataExportConfirm": "Request Export",
"accountDataExportRequested": "Data export requested. You'll receive an email when it's ready.",
"accountDeletionDescription": "Permanently delete your account and all your data", "accountDeletionDescription": "Permanently delete your account and all your data",
"accountSettingsHelp": "Account Settings Help", "accountSettingsHelp": "Account Settings Help",
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.", "accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
"unauthorized": "Unauthorized", "unauthorized": "Unauthorized",
"unauthorizedHint": "You're not signed in or session expired, please sign in again.", "unauthorizedHint": "You're not signed in or session expired, please sign in again.",
"publisherVisitAccountPage": "Visit the profile of {}" "publisherVisitAccountPage": "Visit the profile of {}",
"postVisibility": "Visibility",
"postVisibilityPublic": "Public",
"postVisibilityFriends": "Friends Only",
"postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor",
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
"authFactorDisable": "Disable the Factor",
"authFactorDisableHint": "Are you sure you want to disable this authentication factor? You can enable it again later.",
"authFactorEnable": "Enable the Factor",
"authFactorEnableHint": "Please enter the code that generated by the factor to enable it.",
"authFactorNew": "Create Auth Factor",
"authFactorSecret": "Secret",
"authFactorSecretHint": "Create an secret for this factor.",
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
"authFactorNoQrCode": "No QR code available for this authentication factor",
"cancel": "Cancel",
"confirm": "Confirm",
"authFactorAdditional": "One more step",
"authFactorHint": "Contact method hint",
"authFactorHintHelper": "You need provide a part of your contact method and we will send the verification code to that contact method if it matched our records",
"authSessions": "Active Sessions",
"authSessionsDescription": "See devices you currently logged in.",
"authSessionsCount": {
"one": "{} session",
"other": "{} sessions"
},
"authDeviceCurrent": "Current device",
"lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"authDeviceEditLabel": "Edit Label",
"authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device",
"authDeviceSwipeEditHint": "Swipe left to edit label",
"authDeviceSwipeLogoutHint": "Swipe right to logout device",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"settingsAppearance": "Appearance",
"settingsServer": "Server",
"settingsBehavior": "Behavior",
"settingsDesktop": "Desktop",
"settingsKeyboardShortcuts": "Keyboard Shortcuts",
"settingsEnterToSendDesktopHint": "Press Enter to send messages, use Shift+Enter for new line.",
"settingsHelp": "Settings Help",
"settingsHelpContent": "This page allows you to manage your app settings, appearance, and behavior. If you need assistance, please contact support.",
"settingsKeyboardShortcutSearch": "Search",
"settingsKeyboardShortcutSettings": "Settings",
"settingsKeyboardShortcutNewMessage": "New Message",
"settingsKeyboardShortcutCloseDialog": "Close Dialog",
"close": "Close",
"contactMethod": "Contact Method",
"contactMethodType": "Contact Type",
"contactMethodTypeEmail": "Email",
"contactMethodTypePhone": "Phone",
"contactMethodTypeAddress": "Address",
"contactMethodEmailHint": "Enter your email address",
"contactMethodPhoneHint": "Enter your phone number",
"contactMethodAddressHint": "Enter your physical address",
"contactMethodEmailDescription": "Your email will be used for account recovery and notifications",
"contactMethodPhoneDescription": "Your phone number will be used for account recovery and notifications",
"contactMethodAddressDescription": "Your physical address will be used for shipping and billing purposes.",
"contactMethodVerified": "Verified",
"contactMethodUnverified": "Unverified",
"contactMethodVerify": "Verify Contact",
"contactMethodDelete": "Delete Contact",
"contactMethodNew": "New Contact Method",
"contactMethodContentEmpty": "Contact content cannot be empty",
"contactMethodVerificationSent": "Verification code sent to your contact method",
"contactMethodVerificationNeeded": "The contact method is added, but not verified yet. You can verify it by tapping it and select verify.",
"accountContactMethod": "Contact Methods",
"accountContactMethodDescription": "Manage your contact methods for account recovery and notifications",
"authFactorVerificationNeeded": "The auth factor is added, but it is not enabled yet. You can enable it by tapping it and enter the verification code.",
"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."
} }

View File

@ -99,6 +99,14 @@
"permissionMember": "成员", "permissionMember": "成员",
"reply": "回复", "reply": "回复",
"forward": "转发", "forward": "转发",
"repliedTo": "回复了",
"forwarded": "转发了",
"hasAttachments": {
"other": "{}个附件"
},
"postHasAttachments": {
"other": "{}个附件"
},
"edited": "已编辑", "edited": "已编辑",
"addVideo": "添加视频", "addVideo": "添加视频",
"addPhoto": "添加照片", "addPhoto": "添加照片",
@ -278,5 +286,10 @@
"settingsHideBottomNav": "隐藏底部导航", "settingsHideBottomNav": "隐藏底部导航",
"settingsSoundEffects": "音效", "settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人节功能", "settingsAprilFoolFeatures": "愚人节功能",
"settingsEnterToSend": "按下 Enter 发送" "settingsEnterToSend": "按下 Enter 发送",
"postVisibility": "可见性",
"postVisibilityPublic": "公开",
"postVisibilityFriends": "仅好友可见",
"postVisibilityUnlisted": "不公开",
"postVisibilityPrivate": "私密"
} }

View File

@ -99,6 +99,14 @@
"permissionMember": "成員", "permissionMember": "成員",
"reply": "回覆", "reply": "回覆",
"forward": "轉發", "forward": "轉發",
"repliedTo": "回覆了",
"forwarded": "轉發了",
"hasAttachments": {
"other": "{}個附件"
},
"postHasAttachments": {
"other": "{}個附件"
},
"edited": "已編輯", "edited": "已編輯",
"addVideo": "新增影片", "addVideo": "新增影片",
"addPhoto": "新增照片", "addPhoto": "新增照片",
@ -278,5 +286,10 @@
"settingsHideBottomNav": "隱藏底部導航", "settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效", "settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能", "settingsAprilFoolFeatures": "愚人節功能",
"settingsEnterToSend": "按下 Enter 傳送" "settingsEnterToSend": "按下 Enter 傳送",
"postVisibility": "可見性",
"postVisibilityPublic": "公開",
"postVisibilityFriends": "僅好友可見",
"postVisibilityUnlisted": "不公開",
"postVisibilityPrivate": "私密"
} }

View File

@ -0,0 +1,4 @@
#!/bin/sh
cd "$(dirname "$0")"
exec ./island

View File

@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Name=Solian
Exec=island %u
Icon=icon-padded
Categories=Network;

Binary file not shown.

View File

@ -90,6 +90,8 @@ PODS:
- flutter_webrtc (0.14.0): - flutter_webrtc (0.14.0):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.07) - WebRTC-SDK (= 125.6422.07)
- gal (1.0.0):
- Flutter
- GoogleDataTransport (10.1.0): - GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
@ -144,6 +146,8 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- record_ios (1.0.0):
- Flutter
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- SDWebImage (5.21.0): - SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0) - SDWebImage/Core (= 5.21.0)
@ -201,6 +205,7 @@ DEPENDENCIES:
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- Kingfisher (~> 8.0) - Kingfisher (~> 8.0)
@ -210,6 +215,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - 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`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
@ -265,6 +271,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_udid/ios" :path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc: flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios" :path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/ios"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
irondash_engine_context: irondash_engine_context:
@ -281,6 +289,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/pasteboard/ios" :path: ".symlinks/plugins/pasteboard/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
record_ios:
:path: ".symlinks/plugins/record_ios/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin: sqflite_darwin:
@ -317,6 +327,7 @@ SPEC CHECKSUMS:
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117 flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117
gal: 29e711cd17bccb47f839d9b30afe9bc9750950b2
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
@ -331,6 +342,7 @@ SPEC CHECKSUMS:
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7

View File

@ -113,6 +113,7 @@
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
CloudFile.swift, CloudFile.swift,
DataExchange.swift,
); );
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */; target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
}; };

View File

@ -10,6 +10,7 @@ import UIKit
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
UNUserNotificationCenter.current().delegate = notifyDelegate UNUserNotificationCenter.current().delegate = notifyDelegate
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }

View File

@ -66,5 +66,10 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -16,17 +16,12 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
return return
} }
var token: String = "" var token: String? = UserDefaults.standard.getFlutterToken()
if let tokenJson = UserDefaults.standard.string(forKey: "dyn_user_tk"), if token == nil {
let tokenData = tokenJson.data(using: String.Encoding.utf8),
let tokenDict = try? JSONSerialization.jsonObject(with: tokenData) as? [String: Any],
let tokenValue = tokenDict["token"] as? String {
token = tokenValue
} else {
return return
} }
let serverUrl = "https://nt.solian.app" let serverUrl = UserDefaults.standard.getServerUrl()
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages" let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
let parameters: [String: Any?] = [ let parameters: [String: Any?] = [
@ -35,7 +30,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
] ]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders( AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
[HTTPHeader(name: "Authorization", value: "AtField \(token)")] [HTTPHeader(name: "Authorization", value: "AtField \(token!)")]
)) ))
.validate() .validate()
.responseString { response in .responseString { response in

View File

@ -0,0 +1,31 @@
//
// DataExchange.swift
// Runner
//
// Created by LittleSheep on 2025/6/2.
//
import Foundation
extension UserDefaults {
func getFlutterValue<T>(forKey key: String) -> T? {
let prefixedKey = "flutter.\(key)"
return self.object(forKey: prefixedKey) as? T
}
func getFlutterToken(forKey key: String = "dyn_user_tk") -> String? {
let prefixedKey = "flutter.\(key)"
guard let jsonString = self.string(forKey: prefixedKey),
let data = jsonString.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data),
let jsonDict = jsonObject as? [String: Any],
let token = jsonDict["token"] as? String else {
return nil
}
return token
}
func getServerUrl(forKey key: String = "app_server_url") -> String {
return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app"
}
}

View File

@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:island/database/drift_db.dart'; import 'package:island/database/drift_db.dart';
import 'package:island/database/message.dart'; import 'package:island/database/message.dart';
@ -45,6 +48,13 @@ 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); final response = MessageSyncResponse.fromJson(resp.data);
for (final change in response.changes) { for (final change in response.changes) {
switch (change.action) { switch (change.action) {
@ -231,7 +241,7 @@ class MessageRepository {
for (var idx = 0; idx < attachments.length; idx++) { for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: attachments[idx].data, fileData: attachments[idx],
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachments[idx].data.name ?? 'Post media', filename: attachments[idx].data.name ?? 'Post media',
@ -299,7 +309,7 @@ class MessageRepository {
} }
Future<LocalChatMessage> retryMessage(String pendingMessageId) async { Future<LocalChatMessage> retryMessage(String pendingMessageId) async {
final message = pendingMessages[pendingMessageId]; final message = await getMessageById(pendingMessageId);
if (message == null) { if (message == null) {
throw Exception('Message not found'); throw Exception('Message not found');
} }

View File

@ -10,13 +10,10 @@ sealed class SnActivity with _$SnActivity {
required String id, required String id,
required String type, required String type,
required String resourceIdentifier, required String resourceIdentifier,
required int visibility,
required String accountId,
required SnAccount account,
required dynamic data, required dynamic data,
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
required dynamic deletedAt, required DateTime? deletedAt,
}) = _SnActivity; }) = _SnActivity;
factory SnActivity.fromJson(Map<String, dynamic> json) => factory SnActivity.fromJson(Map<String, dynamic> json) =>

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnActivity { mixin _$SnActivity {
String get id; String get type; String get resourceIdentifier; int get visibility; String get accountId; SnAccount get account; dynamic get data; DateTime get createdAt; DateTime get updatedAt; dynamic get deletedAt; String get id; String get type; String get resourceIdentifier; dynamic get data; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnActivity /// Create a copy of SnActivity
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnActivityCopyWith<SnActivity> get copyWith => _$SnActivityCopyWithImpl<SnActiv
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.deletedAt, deletedAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&const DeepCollectionEquality().equals(other.data, data)&&(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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,visibility,accountId,account,const DeepCollectionEquality().hash(data),createdAt,updatedAt,const DeepCollectionEquality().hash(deletedAt)); int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,const DeepCollectionEquality().hash(data),createdAt,updatedAt,deletedAt);
@override @override
String toString() { String toString() {
return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, visibility: $visibility, accountId: $accountId, account: $account, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@ -49,11 +49,11 @@ abstract mixin class $SnActivityCopyWith<$Res> {
factory $SnActivityCopyWith(SnActivity value, $Res Function(SnActivity) _then) = _$SnActivityCopyWithImpl; factory $SnActivityCopyWith(SnActivity value, $Res Function(SnActivity) _then) = _$SnActivityCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String type, String resourceIdentifier, int visibility, String accountId, SnAccount account, dynamic data, DateTime createdAt, DateTime updatedAt, dynamic deletedAt String id, String type, String resourceIdentifier, dynamic data, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
$SnAccountCopyWith<$Res> get account;
} }
/// @nodoc /// @nodoc
@ -66,31 +66,19 @@ class _$SnActivityCopyWithImpl<$Res>
/// Create a copy of SnActivity /// Create a copy of SnActivity
/// with the given fields replaced by the non-null parameter values. /// 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? resourceIdentifier = null,Object? visibility = null,Object? accountId = null,Object? account = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? resourceIdentifier = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as 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,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic, as DateTime?,
)); ));
} }
/// Create a copy of SnActivity
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get account {
return $SnAccountCopyWith<$Res>(_self.account, (value) {
return _then(_self.copyWith(account: value));
});
}
} }
@ -98,19 +86,16 @@ $SnAccountCopyWith<$Res> get account {
@JsonSerializable() @JsonSerializable()
class _SnActivity implements SnActivity { class _SnActivity implements SnActivity {
const _SnActivity({required this.id, required this.type, required this.resourceIdentifier, required this.visibility, required this.accountId, required this.account, required this.data, required this.createdAt, required this.updatedAt, required this.deletedAt}); const _SnActivity({required this.id, required this.type, required this.resourceIdentifier, required this.data, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnActivity.fromJson(Map<String, dynamic> json) => _$SnActivityFromJson(json); factory _SnActivity.fromJson(Map<String, dynamic> json) => _$SnActivityFromJson(json);
@override final String id; @override final String id;
@override final String type; @override final String type;
@override final String resourceIdentifier; @override final String resourceIdentifier;
@override final int visibility;
@override final String accountId;
@override final SnAccount account;
@override final dynamic data; @override final dynamic data;
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@override final dynamic deletedAt; @override final DateTime? deletedAt;
/// Create a copy of SnActivity /// Create a copy of SnActivity
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -125,16 +110,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.deletedAt, deletedAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&const DeepCollectionEquality().equals(other.data, data)&&(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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,visibility,accountId,account,const DeepCollectionEquality().hash(data),createdAt,updatedAt,const DeepCollectionEquality().hash(deletedAt)); int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,const DeepCollectionEquality().hash(data),createdAt,updatedAt,deletedAt);
@override @override
String toString() { String toString() {
return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, visibility: $visibility, accountId: $accountId, account: $account, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@ -145,11 +130,11 @@ abstract mixin class _$SnActivityCopyWith<$Res> implements $SnActivityCopyWith<$
factory _$SnActivityCopyWith(_SnActivity value, $Res Function(_SnActivity) _then) = __$SnActivityCopyWithImpl; factory _$SnActivityCopyWith(_SnActivity value, $Res Function(_SnActivity) _then) = __$SnActivityCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String type, String resourceIdentifier, int visibility, String accountId, SnAccount account, dynamic data, DateTime createdAt, DateTime updatedAt, dynamic deletedAt String id, String type, String resourceIdentifier, dynamic data, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@override $SnAccountCopyWith<$Res> get account;
} }
/// @nodoc /// @nodoc
@ -162,32 +147,20 @@ class __$SnActivityCopyWithImpl<$Res>
/// Create a copy of SnActivity /// Create a copy of SnActivity
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? resourceIdentifier = null,Object? visibility = null,Object? accountId = null,Object? account = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? resourceIdentifier = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnActivity( return _then(_SnActivity(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as 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,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic, as DateTime?,
)); ));
} }
/// Create a copy of SnActivity
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get account {
return $SnAccountCopyWith<$Res>(_self.account, (value) {
return _then(_self.copyWith(account: value));
});
}
} }

View File

@ -10,13 +10,13 @@ _SnActivity _$SnActivityFromJson(Map<String, dynamic> json) => _SnActivity(
id: json['id'] as String, id: json['id'] as String,
type: json['type'] as String, type: json['type'] as String,
resourceIdentifier: json['resource_identifier'] as String, resourceIdentifier: json['resource_identifier'] as String,
visibility: (json['visibility'] as num).toInt(),
accountId: json['account_id'] as String,
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
data: json['data'], data: json['data'],
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'], deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
); );
Map<String, dynamic> _$SnActivityToJson(_SnActivity instance) => Map<String, dynamic> _$SnActivityToJson(_SnActivity instance) =>
@ -24,13 +24,10 @@ Map<String, dynamic> _$SnActivityToJson(_SnActivity instance) =>
'id': instance.id, 'id': instance.id,
'type': instance.type, 'type': instance.type,
'resource_identifier': instance.resourceIdentifier, 'resource_identifier': instance.resourceIdentifier,
'visibility': instance.visibility,
'account_id': instance.accountId,
'account': instance.account.toJson(),
'data': instance.data, 'data': instance.data,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt, 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_SnCheckInResult _$SnCheckInResultFromJson(Map<String, dynamic> json) => _SnCheckInResult _$SnCheckInResultFromJson(Map<String, dynamic> json) =>

View File

@ -18,13 +18,18 @@ sealed class SnAuthChallenge with _$SnAuthChallenge {
required DateTime expiredAt, required DateTime expiredAt,
required int stepRemain, required int stepRemain,
required int stepTotal, required int stepTotal,
required int failedAttempts,
required int platform,
required int type,
required List<String> blacklistFactors, required List<String> blacklistFactors,
required List<String> audiences, required List<dynamic> audiences,
required List<String> scopes, required List<dynamic> scopes,
required String ipAddress, required String ipAddress,
required String userAgent, required String userAgent,
required String? deviceId, required String deviceId,
required String? nonce, required String? nonce,
required String? location,
required String accountId,
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
required DateTime? deletedAt, required DateTime? deletedAt,
@ -34,6 +39,25 @@ sealed class SnAuthChallenge with _$SnAuthChallenge {
_$SnAuthChallengeFromJson(json); _$SnAuthChallengeFromJson(json);
} }
@freezed
sealed class SnAuthSession with _$SnAuthSession {
const factory SnAuthSession({
required String id,
required String? label,
required DateTime lastGrantedAt,
required DateTime expiredAt,
required String accountId,
required String challengeId,
required SnAuthChallenge challenge,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAuthSession;
factory SnAuthSession.fromJson(Map<String, dynamic> json) =>
_$SnAuthSessionFromJson(json);
}
@freezed @freezed
sealed class SnAuthFactor with _$SnAuthFactor { sealed class SnAuthFactor with _$SnAuthFactor {
const factory SnAuthFactor({ const factory SnAuthFactor({
@ -42,8 +66,28 @@ sealed class SnAuthFactor with _$SnAuthFactor {
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
required DateTime? deletedAt, required DateTime? deletedAt,
required DateTime? expiredAt,
required DateTime? enabledAt,
required int trustworthy,
required Map<String, dynamic>? createdResponse,
}) = _SnAuthFactor; }) = _SnAuthFactor;
factory SnAuthFactor.fromJson(Map<String, dynamic> json) => factory SnAuthFactor.fromJson(Map<String, dynamic> json) =>
_$SnAuthFactorFromJson(json); _$SnAuthFactorFromJson(json);
} }
@freezed
sealed class SnAuthDevice with _$SnAuthDevice {
const factory SnAuthDevice({
required dynamic label,
required String userAgent,
required String deviceId,
required int platform,
required List<SnAuthSession> sessions,
// Not from backend, used for UI
@Default(false) bool isCurrent,
}) = _SnAuthDevice;
factory SnAuthDevice.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceFromJson(json);
}

View File

@ -149,7 +149,7 @@ as String,
/// @nodoc /// @nodoc
mixin _$SnAuthChallenge { mixin _$SnAuthChallenge {
String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; List<String> get blacklistFactors; List<String> get audiences; List<String> get scopes; String get ipAddress; String get userAgent; String? get deviceId; String? get nonce; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get platform; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String get deviceId; String? get nonce; String? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -162,16 +162,16 @@ $SnAuthChallengeCopyWith<SnAuthChallenge> get copyWith => _$SnAuthChallengeCopyW
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(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 SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,deviceId,nonce,createdAt,updatedAt,deletedAt); int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]);
@override @override
String toString() { String toString() {
return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@ -182,7 +182,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res> {
factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, DateTime expiredAt, int stepRemain, int stepTotal, List<String> blacklistFactors, List<String> audiences, List<String> scopes, String ipAddress, String userAgent, String? deviceId, String? nonce, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@ -199,20 +199,25 @@ class _$SnAuthChallengeCopyWithImpl<$Res>
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = freezed,Object? nonce = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,blacklistFactors: null == blacklistFactors ? _self.blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable as int,blacklistFactors: null == blacklistFactors ? _self.blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable
as List<String>,audiences: null == audiences ? _self.audiences : audiences // ignore: cast_nullable_to_non_nullable as List<String>,audiences: null == audiences ? _self.audiences : audiences // ignore: cast_nullable_to_non_nullable
as List<String>,scopes: null == scopes ? _self.scopes : scopes // ignore: cast_nullable_to_non_nullable as List<dynamic>,scopes: null == scopes ? _self.scopes : scopes // ignore: cast_nullable_to_non_nullable
as List<String>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable
as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: freezed == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String?,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,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
@ -226,13 +231,16 @@ as DateTime?,
@JsonSerializable() @JsonSerializable()
class _SnAuthChallenge implements SnAuthChallenge { class _SnAuthChallenge implements SnAuthChallenge {
const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required final List<String> blacklistFactors, required final List<String> audiences, required final List<String> scopes, required this.ipAddress, required this.userAgent, required this.deviceId, required this.nonce, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes; const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required this.failedAttempts, required this.platform, required this.type, required final List<String> blacklistFactors, required final List<dynamic> audiences, required final List<dynamic> scopes, required this.ipAddress, required this.userAgent, required this.deviceId, required this.nonce, required this.location, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes;
factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json); factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
@override final String id; @override final String id;
@override final DateTime expiredAt; @override final DateTime expiredAt;
@override final int stepRemain; @override final int stepRemain;
@override final int stepTotal; @override final int stepTotal;
@override final int failedAttempts;
@override final int platform;
@override final int type;
final List<String> _blacklistFactors; final List<String> _blacklistFactors;
@override List<String> get blacklistFactors { @override List<String> get blacklistFactors {
if (_blacklistFactors is EqualUnmodifiableListView) return _blacklistFactors; if (_blacklistFactors is EqualUnmodifiableListView) return _blacklistFactors;
@ -240,15 +248,15 @@ class _SnAuthChallenge implements SnAuthChallenge {
return EqualUnmodifiableListView(_blacklistFactors); return EqualUnmodifiableListView(_blacklistFactors);
} }
final List<String> _audiences; final List<dynamic> _audiences;
@override List<String> get audiences { @override List<dynamic> get audiences {
if (_audiences is EqualUnmodifiableListView) return _audiences; if (_audiences is EqualUnmodifiableListView) return _audiences;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_audiences); return EqualUnmodifiableListView(_audiences);
} }
final List<String> _scopes; final List<dynamic> _scopes;
@override List<String> get scopes { @override List<dynamic> get scopes {
if (_scopes is EqualUnmodifiableListView) return _scopes; if (_scopes is EqualUnmodifiableListView) return _scopes;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_scopes); return EqualUnmodifiableListView(_scopes);
@ -256,8 +264,10 @@ class _SnAuthChallenge implements SnAuthChallenge {
@override final String ipAddress; @override final String ipAddress;
@override final String userAgent; @override final String userAgent;
@override final String? deviceId; @override final String deviceId;
@override final String? nonce; @override final String? nonce;
@override final String? location;
@override final String accountId;
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@override final DateTime? deletedAt; @override final DateTime? deletedAt;
@ -275,16 +285,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(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 _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,deviceId,nonce,createdAt,updatedAt,deletedAt); int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]);
@override @override
String toString() { String toString() {
return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@ -295,7 +305,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, DateTime expiredAt, int stepRemain, int stepTotal, List<String> blacklistFactors, List<String> audiences, List<String> scopes, String ipAddress, String userAgent, String? deviceId, String? nonce, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@ -312,20 +322,25 @@ class __$SnAuthChallengeCopyWithImpl<$Res>
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = freezed,Object? nonce = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAuthChallenge( return _then(_SnAuthChallenge(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,blacklistFactors: null == blacklistFactors ? _self._blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable as int,blacklistFactors: null == blacklistFactors ? _self._blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable
as List<String>,audiences: null == audiences ? _self._audiences : audiences // ignore: cast_nullable_to_non_nullable as List<String>,audiences: null == audiences ? _self._audiences : audiences // ignore: cast_nullable_to_non_nullable
as List<String>,scopes: null == scopes ? _self._scopes : scopes // ignore: cast_nullable_to_non_nullable as List<dynamic>,scopes: null == scopes ? _self._scopes : scopes // ignore: cast_nullable_to_non_nullable
as List<String>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable
as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: freezed == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String?,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,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
@ -336,10 +351,188 @@ as DateTime?,
} }
/// @nodoc
mixin _$SnAuthSession {
String get id; String? get label; DateTime get lastGrantedAt; DateTime get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAuthSession
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAuthSessionCopyWith<SnAuthSession> get copyWith => _$SnAuthSessionCopyWithImpl<SnAuthSession>(this as SnAuthSession, _$identity);
/// Serializes this SnAuthSession to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthSession&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.lastGrantedAt, lastGrantedAt) || other.lastGrantedAt == lastGrantedAt)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.challengeId, challengeId) || other.challengeId == challengeId)&&(identical(other.challenge, challenge) || other.challenge == challenge)&&(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,label,lastGrantedAt,expiredAt,accountId,challengeId,challenge,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAuthSession(id: $id, label: $label, lastGrantedAt: $lastGrantedAt, expiredAt: $expiredAt, accountId: $accountId, challengeId: $challengeId, challenge: $challenge, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAuthSessionCopyWith<$Res> {
factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl;
@useResult
$Res call({
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnAuthChallengeCopyWith<$Res> get challenge;
}
/// @nodoc
class _$SnAuthSessionCopyWithImpl<$Res>
implements $SnAuthSessionCopyWith<$Res> {
_$SnAuthSessionCopyWithImpl(this._self, this._then);
final SnAuthSession _self;
final $Res Function(SnAuthSession) _then;
/// Create a copy of SnAuthSession
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
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 SnAuthSession
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAuthChallengeCopyWith<$Res> get challenge {
return $SnAuthChallengeCopyWith<$Res>(_self.challenge, (value) {
return _then(_self.copyWith(challenge: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnAuthSession implements SnAuthSession {
const _SnAuthSession({required this.id, required this.label, required this.lastGrantedAt, required this.expiredAt, required this.accountId, required this.challengeId, required this.challenge, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAuthSession.fromJson(Map<String, dynamic> json) => _$SnAuthSessionFromJson(json);
@override final String id;
@override final String? label;
@override final DateTime lastGrantedAt;
@override final DateTime expiredAt;
@override final String accountId;
@override final String challengeId;
@override final SnAuthChallenge challenge;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAuthSession
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAuthSessionCopyWith<_SnAuthSession> get copyWith => __$SnAuthSessionCopyWithImpl<_SnAuthSession>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAuthSessionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthSession&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.lastGrantedAt, lastGrantedAt) || other.lastGrantedAt == lastGrantedAt)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.challengeId, challengeId) || other.challengeId == challengeId)&&(identical(other.challenge, challenge) || other.challenge == challenge)&&(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,label,lastGrantedAt,expiredAt,accountId,challengeId,challenge,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAuthSession(id: $id, label: $label, lastGrantedAt: $lastGrantedAt, expiredAt: $expiredAt, accountId: $accountId, challengeId: $challengeId, challenge: $challenge, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAuthSessionCopyWith<$Res> implements $SnAuthSessionCopyWith<$Res> {
factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl;
@override @useResult
$Res call({
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnAuthChallengeCopyWith<$Res> get challenge;
}
/// @nodoc
class __$SnAuthSessionCopyWithImpl<$Res>
implements _$SnAuthSessionCopyWith<$Res> {
__$SnAuthSessionCopyWithImpl(this._self, this._then);
final _SnAuthSession _self;
final $Res Function(_SnAuthSession) _then;
/// Create a copy of SnAuthSession
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAuthSession(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
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 SnAuthSession
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAuthChallengeCopyWith<$Res> get challenge {
return $SnAuthChallengeCopyWith<$Res>(_self.challenge, (value) {
return _then(_self.copyWith(challenge: value));
});
}
}
/// @nodoc /// @nodoc
mixin _$SnAuthFactor { mixin _$SnAuthFactor {
String get id; int get type; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; int get type; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; DateTime? get expiredAt; DateTime? get enabledAt; int get trustworthy; Map<String, dynamic>? get createdResponse;
/// Create a copy of SnAuthFactor /// Create a copy of SnAuthFactor
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -352,16 +545,16 @@ $SnAuthFactorCopyWith<SnAuthFactor> get copyWith => _$SnAuthFactorCopyWithImpl<S
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthFactor&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(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 SnAuthFactor&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.enabledAt, enabledAt) || other.enabledAt == enabledAt)&&(identical(other.trustworthy, trustworthy) || other.trustworthy == trustworthy)&&const DeepCollectionEquality().equals(other.createdResponse, createdResponse));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,type,createdAt,updatedAt,deletedAt); int get hashCode => Object.hash(runtimeType,id,type,createdAt,updatedAt,deletedAt,expiredAt,enabledAt,trustworthy,const DeepCollectionEquality().hash(createdResponse));
@override @override
String toString() { String toString() {
return 'SnAuthFactor(id: $id, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAuthFactor(id: $id, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, expiredAt: $expiredAt, enabledAt: $enabledAt, trustworthy: $trustworthy, createdResponse: $createdResponse)';
} }
@ -372,7 +565,7 @@ abstract mixin class $SnAuthFactorCopyWith<$Res> {
factory $SnAuthFactorCopyWith(SnAuthFactor value, $Res Function(SnAuthFactor) _then) = _$SnAuthFactorCopyWithImpl; factory $SnAuthFactorCopyWith(SnAuthFactor value, $Res Function(SnAuthFactor) _then) = _$SnAuthFactorCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, int type, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, int type, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, DateTime? expiredAt, DateTime? enabledAt, int trustworthy, Map<String, dynamic>? createdResponse
}); });
@ -389,14 +582,18 @@ class _$SnAuthFactorCopyWithImpl<$Res>
/// Create a copy of SnAuthFactor /// Create a copy of SnAuthFactor
/// with the given fields replaced by the non-null parameter values. /// 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? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? expiredAt = freezed,Object? enabledAt = freezed,Object? trustworthy = null,Object? createdResponse = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,enabledAt: freezed == enabledAt ? _self.enabledAt : enabledAt // ignore: cast_nullable_to_non_nullable
as DateTime?,trustworthy: null == trustworthy ? _self.trustworthy : trustworthy // ignore: cast_nullable_to_non_nullable
as int,createdResponse: freezed == createdResponse ? _self.createdResponse : createdResponse // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
)); ));
} }
@ -407,7 +604,7 @@ as DateTime?,
@JsonSerializable() @JsonSerializable()
class _SnAuthFactor implements SnAuthFactor { class _SnAuthFactor implements SnAuthFactor {
const _SnAuthFactor({required this.id, required this.type, required this.createdAt, required this.updatedAt, required this.deletedAt}); const _SnAuthFactor({required this.id, required this.type, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.expiredAt, required this.enabledAt, required this.trustworthy, required final Map<String, dynamic>? createdResponse}): _createdResponse = createdResponse;
factory _SnAuthFactor.fromJson(Map<String, dynamic> json) => _$SnAuthFactorFromJson(json); factory _SnAuthFactor.fromJson(Map<String, dynamic> json) => _$SnAuthFactorFromJson(json);
@override final String id; @override final String id;
@ -415,6 +612,18 @@ class _SnAuthFactor implements SnAuthFactor {
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@override final DateTime? deletedAt; @override final DateTime? deletedAt;
@override final DateTime? expiredAt;
@override final DateTime? enabledAt;
@override final int trustworthy;
final Map<String, dynamic>? _createdResponse;
@override Map<String, dynamic>? get createdResponse {
final value = _createdResponse;
if (value == null) return null;
if (_createdResponse is EqualUnmodifiableMapView) return _createdResponse;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
/// Create a copy of SnAuthFactor /// Create a copy of SnAuthFactor
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -429,16 +638,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthFactor&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(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 _SnAuthFactor&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.enabledAt, enabledAt) || other.enabledAt == enabledAt)&&(identical(other.trustworthy, trustworthy) || other.trustworthy == trustworthy)&&const DeepCollectionEquality().equals(other._createdResponse, _createdResponse));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,type,createdAt,updatedAt,deletedAt); int get hashCode => Object.hash(runtimeType,id,type,createdAt,updatedAt,deletedAt,expiredAt,enabledAt,trustworthy,const DeepCollectionEquality().hash(_createdResponse));
@override @override
String toString() { String toString() {
return 'SnAuthFactor(id: $id, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAuthFactor(id: $id, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, expiredAt: $expiredAt, enabledAt: $enabledAt, trustworthy: $trustworthy, createdResponse: $createdResponse)';
} }
@ -449,7 +658,7 @@ abstract mixin class _$SnAuthFactorCopyWith<$Res> implements $SnAuthFactorCopyWi
factory _$SnAuthFactorCopyWith(_SnAuthFactor value, $Res Function(_SnAuthFactor) _then) = __$SnAuthFactorCopyWithImpl; factory _$SnAuthFactorCopyWith(_SnAuthFactor value, $Res Function(_SnAuthFactor) _then) = __$SnAuthFactorCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, int type, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, int type, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, DateTime? expiredAt, DateTime? enabledAt, int trustworthy, Map<String, dynamic>? createdResponse
}); });
@ -466,14 +675,174 @@ class __$SnAuthFactorCopyWithImpl<$Res>
/// Create a copy of SnAuthFactor /// Create a copy of SnAuthFactor
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? expiredAt = freezed,Object? enabledAt = freezed,Object? trustworthy = null,Object? createdResponse = freezed,}) {
return _then(_SnAuthFactor( return _then(_SnAuthFactor(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,enabledAt: freezed == enabledAt ? _self.enabledAt : enabledAt // ignore: cast_nullable_to_non_nullable
as DateTime?,trustworthy: null == trustworthy ? _self.trustworthy : trustworthy // ignore: cast_nullable_to_non_nullable
as int,createdResponse: freezed == createdResponse ? _self._createdResponse : createdResponse // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
));
}
}
/// @nodoc
mixin _$SnAuthDevice {
dynamic get label; String get userAgent; String get deviceId; int get platform; List<SnAuthSession> get sessions;// Not from backend, used for UI
bool get isCurrent;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAuthDeviceCopyWith<SnAuthDevice> get copyWith => _$SnAuthDeviceCopyWithImpl<SnAuthDevice>(this as SnAuthDevice, _$identity);
/// Serializes this SnAuthDevice to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.sessions, sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(sessions),isCurrent);
@override
String toString() {
return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class $SnAuthDeviceCopyWith<$Res> {
factory $SnAuthDeviceCopyWith(SnAuthDevice value, $Res Function(SnAuthDevice) _then) = _$SnAuthDeviceCopyWithImpl;
@useResult
$Res call({
dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent
});
}
/// @nodoc
class _$SnAuthDeviceCopyWithImpl<$Res>
implements $SnAuthDeviceCopyWith<$Res> {
_$SnAuthDeviceCopyWithImpl(this._self, this._then);
final SnAuthDevice _self;
final $Res Function(SnAuthDevice) _then;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
return _then(_self.copyWith(
label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,sessions: null == sessions ? _self.sessions : sessions // ignore: cast_nullable_to_non_nullable
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnAuthDevice implements SnAuthDevice {
const _SnAuthDevice({required this.label, required this.userAgent, required this.deviceId, required this.platform, required final List<SnAuthSession> sessions, this.isCurrent = false}): _sessions = sessions;
factory _SnAuthDevice.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceFromJson(json);
@override final dynamic label;
@override final String userAgent;
@override final String deviceId;
@override final int platform;
final List<SnAuthSession> _sessions;
@override List<SnAuthSession> get sessions {
if (_sessions is EqualUnmodifiableListView) return _sessions;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_sessions);
}
// Not from backend, used for UI
@override@JsonKey() final bool isCurrent;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAuthDeviceCopyWith<_SnAuthDevice> get copyWith => __$SnAuthDeviceCopyWithImpl<_SnAuthDevice>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAuthDeviceToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._sessions, _sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(_sessions),isCurrent);
@override
String toString() {
return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class _$SnAuthDeviceCopyWith<$Res> implements $SnAuthDeviceCopyWith<$Res> {
factory _$SnAuthDeviceCopyWith(_SnAuthDevice value, $Res Function(_SnAuthDevice) _then) = __$SnAuthDeviceCopyWithImpl;
@override @useResult
$Res call({
dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent
});
}
/// @nodoc
class __$SnAuthDeviceCopyWithImpl<$Res>
implements _$SnAuthDeviceCopyWith<$Res> {
__$SnAuthDeviceCopyWithImpl(this._self, this._then);
final _SnAuthDevice _self;
final $Res Function(_SnAuthDevice) _then;
/// Create a copy of SnAuthDevice
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
return _then(_SnAuthDevice(
label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,sessions: null == sessions ? _self._sessions : sessions // ignore: cast_nullable_to_non_nullable
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }

View File

@ -19,18 +19,21 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
expiredAt: DateTime.parse(json['expired_at'] as String), expiredAt: DateTime.parse(json['expired_at'] as String),
stepRemain: (json['step_remain'] as num).toInt(), stepRemain: (json['step_remain'] as num).toInt(),
stepTotal: (json['step_total'] as num).toInt(), stepTotal: (json['step_total'] as num).toInt(),
failedAttempts: (json['failed_attempts'] as num).toInt(),
platform: (json['platform'] as num).toInt(),
type: (json['type'] as num).toInt(),
blacklistFactors: blacklistFactors:
(json['blacklist_factors'] as List<dynamic>) (json['blacklist_factors'] as List<dynamic>)
.map((e) => e as String) .map((e) => e as String)
.toList(), .toList(),
audiences: audiences: json['audiences'] as List<dynamic>,
(json['audiences'] as List<dynamic>).map((e) => e as String).toList(), scopes: json['scopes'] as List<dynamic>,
scopes:
(json['scopes'] as List<dynamic>).map((e) => e as String).toList(),
ipAddress: json['ip_address'] as String, ipAddress: json['ip_address'] as String,
userAgent: json['user_agent'] as String, userAgent: json['user_agent'] as String,
deviceId: json['device_id'] as String?, deviceId: json['device_id'] as String,
nonce: json['nonce'] as String?, nonce: json['nonce'] as String?,
location: json['location'] as String?,
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt:
@ -45,6 +48,9 @@ Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
'expired_at': instance.expiredAt.toIso8601String(), 'expired_at': instance.expiredAt.toIso8601String(),
'step_remain': instance.stepRemain, 'step_remain': instance.stepRemain,
'step_total': instance.stepTotal, 'step_total': instance.stepTotal,
'failed_attempts': instance.failedAttempts,
'platform': instance.platform,
'type': instance.type,
'blacklist_factors': instance.blacklistFactors, 'blacklist_factors': instance.blacklistFactors,
'audiences': instance.audiences, 'audiences': instance.audiences,
'scopes': instance.scopes, 'scopes': instance.scopes,
@ -52,6 +58,41 @@ Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
'user_agent': instance.userAgent, 'user_agent': instance.userAgent,
'device_id': instance.deviceId, 'device_id': instance.deviceId,
'nonce': instance.nonce, 'nonce': instance.nonce,
'location': instance.location,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnAuthSession _$SnAuthSessionFromJson(Map<String, dynamic> json) =>
_SnAuthSession(
id: json['id'] as String,
label: json['label'] as String?,
lastGrantedAt: DateTime.parse(json['last_granted_at'] as String),
expiredAt: DateTime.parse(json['expired_at'] as String),
accountId: json['account_id'] as String,
challengeId: json['challenge_id'] as String,
challenge: SnAuthChallenge.fromJson(
json['challenge'] as Map<String, dynamic>,
),
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> _$SnAuthSessionToJson(_SnAuthSession instance) =>
<String, dynamic>{
'id': instance.id,
'label': instance.label,
'last_granted_at': instance.lastGrantedAt.toIso8601String(),
'expired_at': instance.expiredAt.toIso8601String(),
'account_id': instance.accountId,
'challenge_id': instance.challengeId,
'challenge': instance.challenge.toJson(),
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
@ -67,6 +108,16 @@ _SnAuthFactor _$SnAuthFactorFromJson(Map<String, dynamic> json) =>
json['deleted_at'] == null json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
enabledAt:
json['enabled_at'] == null
? null
: DateTime.parse(json['enabled_at'] as String),
trustworthy: (json['trustworthy'] as num).toInt(),
createdResponse: json['created_response'] as Map<String, dynamic>?,
); );
Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) => Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) =>
@ -76,4 +127,31 @@ Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) =>
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
'expired_at': instance.expiredAt?.toIso8601String(),
'enabled_at': instance.enabledAt?.toIso8601String(),
'trustworthy': instance.trustworthy,
'created_response': instance.createdResponse,
};
_SnAuthDevice _$SnAuthDeviceFromJson(Map<String, dynamic> json) =>
_SnAuthDevice(
label: json['label'],
userAgent: json['user_agent'] as String,
deviceId: json['device_id'] as String,
platform: (json['platform'] as num).toInt(),
sessions:
(json['sessions'] as List<dynamic>)
.map((e) => SnAuthSession.fromJson(e as Map<String, dynamic>))
.toList(),
isCurrent: json['is_current'] as bool? ?? false,
);
Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) =>
<String, dynamic>{
'label': instance.label,
'user_agent': instance.userAgent,
'device_id': instance.deviceId,
'platform': instance.platform,
'sessions': instance.sessions.map((e) => e.toJson()).toList(),
'is_current': instance.isCurrent,
}; };

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:developer';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
@ -88,6 +91,7 @@ sealed class SnChatMember with _$SnChatMember {
required int notify, required int notify,
required DateTime? joinedAt, required DateTime? joinedAt,
required bool isBot, required bool isBot,
DateTime? lastTyped,
}) = _SnChatMember; }) = _SnChatMember;
factory SnChatMember.fromJson(Map<String, dynamic> json) => factory SnChatMember.fromJson(Map<String, dynamic> json) =>

View File

@ -663,7 +663,7 @@ $SnChatMemberCopyWith<$Res> get sender {
/// @nodoc /// @nodoc
mixin _$SnChatMember { 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 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;
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -676,16 +676,16 @@ $SnChatMemberCopyWith<SnChatMember> get copyWith => _$SnChatMemberCopyWithImpl<S
@override @override
bool operator ==(Object other) { 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)); 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));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot); int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped);
@override @override
String toString() { 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)'; 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)';
} }
@ -696,7 +696,7 @@ abstract mixin class $SnChatMemberCopyWith<$Res> {
factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl; factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl;
@useResult @useResult
$Res call({ $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 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
}); });
@ -713,7 +713,7 @@ class _$SnChatMemberCopyWithImpl<$Res>
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// 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,}) { @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,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable 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,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -728,7 +728,8 @@ as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_no
as int,notify: null == notify ? _self.notify : notify // 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 int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable
as bool, as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable
as DateTime?,
)); ));
} }
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
@ -760,7 +761,7 @@ $SnAccountCopyWith<$Res> get account {
@JsonSerializable() @JsonSerializable()
class _SnChatMember implements SnChatMember { 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}); 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});
factory _SnChatMember.fromJson(Map<String, dynamic> json) => _$SnChatMemberFromJson(json); factory _SnChatMember.fromJson(Map<String, dynamic> json) => _$SnChatMemberFromJson(json);
@override final DateTime createdAt; @override final DateTime createdAt;
@ -776,6 +777,7 @@ class _SnChatMember implements SnChatMember {
@override final int notify; @override final int notify;
@override final DateTime? joinedAt; @override final DateTime? joinedAt;
@override final bool isBot; @override final bool isBot;
@override final DateTime? lastTyped;
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -790,16 +792,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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)); 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));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot); int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped);
@override @override
String toString() { 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)'; 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)';
} }
@ -810,7 +812,7 @@ abstract mixin class _$SnChatMemberCopyWith<$Res> implements $SnChatMemberCopyWi
factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl; factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 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
}); });
@ -827,7 +829,7 @@ class __$SnChatMemberCopyWithImpl<$Res>
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? 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,}) { @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,}) {
return _then(_SnChatMember( return _then(_SnChatMember(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable 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,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -842,7 +844,8 @@ as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_no
as int,notify: null == notify ? _self.notify : notify // 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 int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable
as bool, as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable
as DateTime?,
)); ));
} }

View File

@ -167,6 +167,10 @@ _SnChatMember _$SnChatMemberFromJson(Map<String, dynamic> json) =>
? null ? null
: DateTime.parse(json['joined_at'] as String), : DateTime.parse(json['joined_at'] as String),
isBot: json['is_bot'] as bool, isBot: json['is_bot'] as bool,
lastTyped:
json['last_typed'] == null
? null
: DateTime.parse(json['last_typed'] as String),
); );
Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) => Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
@ -184,6 +188,7 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
'notify': instance.notify, 'notify': instance.notify,
'joined_at': instance.joinedAt?.toIso8601String(), 'joined_at': instance.joinedAt?.toIso8601String(),
'is_bot': instance.isBot, 'is_bot': instance.isBot,
'last_typed': instance.lastTyped?.toIso8601String(),
}; };
_SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) => _SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) =>

View File

@ -85,6 +85,24 @@ sealed class SnAccountBadge with _$SnAccountBadge {
_$SnAccountBadgeFromJson(json); _$SnAccountBadgeFromJson(json);
} }
@freezed
sealed class SnContactMethod with _$SnContactMethod {
const factory SnContactMethod({
required String id,
required int type,
required DateTime? verifiedAt,
required bool isPrimary,
required String content,
required String accountId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnContactMethod;
factory SnContactMethod.fromJson(Map<String, dynamic> json) =>
_$SnContactMethodFromJson(json);
}
@freezed @freezed
sealed class SnNotification with _$SnNotification { sealed class SnNotification with _$SnNotification {
const factory SnNotification({ const factory SnNotification({

View File

@ -746,6 +746,163 @@ as DateTime?,
} }
/// @nodoc
mixin _$SnContactMethod {
String get id; int get type; DateTime? get verifiedAt; bool get isPrimary; String get content; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnContactMethod
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnContactMethodCopyWith<SnContactMethod> get copyWith => _$SnContactMethodCopyWithImpl<SnContactMethod>(this as SnContactMethod, _$identity);
/// Serializes this SnContactMethod to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnContactMethod&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.content, content) || other.content == content)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,verifiedAt,isPrimary,content,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnContactMethod(id: $id, type: $type, verifiedAt: $verifiedAt, isPrimary: $isPrimary, content: $content, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnContactMethodCopyWith<$Res> {
factory $SnContactMethodCopyWith(SnContactMethod value, $Res Function(SnContactMethod) _then) = _$SnContactMethodCopyWithImpl;
@useResult
$Res call({
String id, int type, DateTime? verifiedAt, bool isPrimary, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnContactMethodCopyWithImpl<$Res>
implements $SnContactMethodCopyWith<$Res> {
_$SnContactMethodCopyWithImpl(this._self, this._then);
final SnContactMethod _self;
final $Res Function(SnContactMethod) _then;
/// Create a copy of SnContactMethod
/// 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? verifiedAt = freezed,Object? isPrimary = null,Object? content = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,verifiedAt: freezed == verifiedAt ? _self.verifiedAt : verifiedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isPrimary: null == isPrimary ? _self.isPrimary : isPrimary // ignore: cast_nullable_to_non_nullable
as bool,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String,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?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnContactMethod implements SnContactMethod {
const _SnContactMethod({required this.id, required this.type, required this.verifiedAt, required this.isPrimary, required this.content, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnContactMethod.fromJson(Map<String, dynamic> json) => _$SnContactMethodFromJson(json);
@override final String id;
@override final int type;
@override final DateTime? verifiedAt;
@override final bool isPrimary;
@override final String content;
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnContactMethod
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnContactMethodCopyWith<_SnContactMethod> get copyWith => __$SnContactMethodCopyWithImpl<_SnContactMethod>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnContactMethodToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnContactMethod&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.content, content) || other.content == content)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,verifiedAt,isPrimary,content,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnContactMethod(id: $id, type: $type, verifiedAt: $verifiedAt, isPrimary: $isPrimary, content: $content, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnContactMethodCopyWith<$Res> implements $SnContactMethodCopyWith<$Res> {
factory _$SnContactMethodCopyWith(_SnContactMethod value, $Res Function(_SnContactMethod) _then) = __$SnContactMethodCopyWithImpl;
@override @useResult
$Res call({
String id, int type, DateTime? verifiedAt, bool isPrimary, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnContactMethodCopyWithImpl<$Res>
implements _$SnContactMethodCopyWith<$Res> {
__$SnContactMethodCopyWithImpl(this._self, this._then);
final _SnContactMethod _self;
final $Res Function(_SnContactMethod) _then;
/// Create a copy of SnContactMethod
/// 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? verifiedAt = freezed,Object? isPrimary = null,Object? content = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnContactMethod(
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
as int,verifiedAt: freezed == verifiedAt ? _self.verifiedAt : verifiedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isPrimary: null == isPrimary ? _self.isPrimary : isPrimary // ignore: cast_nullable_to_non_nullable
as bool,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String,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?,
));
}
}
/// @nodoc /// @nodoc
mixin _$SnNotification { mixin _$SnNotification {

View File

@ -157,6 +157,38 @@ Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_SnContactMethod _$SnContactMethodFromJson(Map<String, dynamic> json) =>
_SnContactMethod(
id: json['id'] as String,
type: (json['type'] as num).toInt(),
verifiedAt:
json['verified_at'] == null
? null
: DateTime.parse(json['verified_at'] as String),
isPrimary: json['is_primary'] as bool,
content: json['content'] as String,
accountId: json['account_id'] 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> _$SnContactMethodToJson(_SnContactMethod instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'verified_at': instance.verifiedAt?.toIso8601String(),
'is_primary': instance.isPrimary,
'content': instance.content,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) => _SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) =>
_SnNotification( _SnNotification(
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),

View File

@ -329,7 +329,7 @@ class ChatListRouteArgs {
/// [_i7.ChatRoomScreen] /// [_i7.ChatRoomScreen]
class ChatRoomRoute extends _i27.PageRouteInfo<ChatRoomRouteArgs> { class ChatRoomRoute extends _i27.PageRouteInfo<ChatRoomRouteArgs> {
ChatRoomRoute({ ChatRoomRoute({
_i28.Key? key, _i29.Key? key,
required String id, required String id,
List<_i27.PageRouteInfo>? children, List<_i27.PageRouteInfo>? children,
}) : super( }) : super(
@ -356,7 +356,7 @@ class ChatRoomRoute extends _i27.PageRouteInfo<ChatRoomRouteArgs> {
class ChatRoomRouteArgs { class ChatRoomRouteArgs {
const ChatRoomRouteArgs({this.key, required this.id}); const ChatRoomRouteArgs({this.key, required this.id});
final _i28.Key? key; final _i29.Key? key;
final String id; final String id;
@ -1071,10 +1071,17 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
PostComposeRoute({ PostComposeRoute({
_i28.Key? key, _i28.Key? key,
_i30.SnPost? originalPost, _i30.SnPost? originalPost,
_i30.SnPost? repliedPost,
_i30.SnPost? forwardedPost,
List<_i27.PageRouteInfo>? children, List<_i27.PageRouteInfo>? children,
}) : super( }) : super(
PostComposeRoute.name, PostComposeRoute.name,
args: PostComposeRouteArgs(key: key, originalPost: originalPost), args: PostComposeRouteArgs(
key: key,
originalPost: originalPost,
repliedPost: repliedPost,
forwardedPost: forwardedPost,
),
initialChildren: children, initialChildren: children,
); );
@ -1089,32 +1096,50 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
return _i18.PostComposeScreen( return _i18.PostComposeScreen(
key: args.key, key: args.key,
originalPost: args.originalPost, originalPost: args.originalPost,
repliedPost: args.repliedPost,
forwardedPost: args.forwardedPost,
); );
}, },
); );
} }
class PostComposeRouteArgs { class PostComposeRouteArgs {
const PostComposeRouteArgs({this.key, this.originalPost}); const PostComposeRouteArgs({
this.key,
this.originalPost,
this.repliedPost,
this.forwardedPost,
});
final _i28.Key? key; final _i28.Key? key;
final _i30.SnPost? originalPost; final _i30.SnPost? originalPost;
final _i30.SnPost? repliedPost;
final _i30.SnPost? forwardedPost;
@override @override
String toString() { String toString() {
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost}'; return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost}';
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is! PostComposeRouteArgs) return false; if (other is! PostComposeRouteArgs) return false;
return key == other.key && originalPost == other.originalPost; return key == other.key &&
originalPost == other.originalPost &&
repliedPost == other.repliedPost &&
forwardedPost == other.forwardedPost;
} }
@override @override
int get hashCode => key.hashCode ^ originalPost.hashCode; int get hashCode =>
key.hashCode ^
originalPost.hashCode ^
repliedPost.hashCode ^
forwardedPost.hashCode;
} }
/// generated route for /// generated route for

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/annotations.dart'; import 'package:auto_route/annotations.dart';
@ -5,16 +6,45 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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: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/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/screens/auth/captcha.dart'; import 'package:island/screens/auth/captcha.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/services/responsive.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/alert.dart';
import 'package:island/widgets/app_scaffold.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: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'; import 'package:styled_widget/styled_widget.dart';
part 'settings.g.dart';
@riverpod
Future<List<SnAuthFactor>> authFactors(Ref ref) async {
final client = ref.read(apiClientProvider);
final res = await client.get('/accounts/me/factors');
return res.data.map<SnAuthFactor>((e) => SnAuthFactor.fromJson(e)).toList();
}
@riverpod
Future<List<SnContactMethod>> contactMethods(Ref ref) async {
final client = ref.read(apiClientProvider);
final resp = await client.get('/accounts/me/contacts');
return resp.data
.map<SnContactMethod>((e) => SnContactMethod.fromJson(e))
.toList();
}
@RoutePage() @RoutePage()
class AccountSettingsScreen extends HookConsumerWidget { class AccountSettingsScreen extends HookConsumerWidget {
const AccountSettingsScreen({super.key}); const AccountSettingsScreen({super.key});
@ -32,6 +62,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
); );
if (!confirm || !context.mounted) return; if (!confirm || !context.mounted) return;
try { try {
showLoadingModal(context);
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
await client.delete('/accounts/me'); await client.delete('/accounts/me');
if (context.mounted) { if (context.mounted) {
@ -39,13 +70,15 @@ class AccountSettingsScreen extends HookConsumerWidget {
} }
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
} }
} }
Future<void> requestResetPassword() async { Future<void> requestResetPassword() async {
final confirm = await showConfirmAlert( final confirm = await showConfirmAlert(
'accountPasswordChangeDescription'.tr(), 'accountPasswordChangeDescription'.tr(),
'accountPassword'.tr(), 'accountPasswordChange'.tr(),
); );
if (!confirm || !context.mounted) return; if (!confirm || !context.mounted) return;
final captchaTk = await Navigator.of( final captchaTk = await Navigator.of(
@ -53,6 +86,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
).push(MaterialPageRoute(builder: (context) => CaptchaScreen())); ).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
if (captchaTk == null) return; if (captchaTk == null) return;
try { try {
showLoadingModal(context);
final userInfo = ref.read(userInfoProvider); final userInfo = ref.read(userInfoProvider);
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
await client.post( await client.post(
@ -64,84 +98,237 @@ class AccountSettingsScreen extends HookConsumerWidget {
} }
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
} }
} }
final authFactors = ref.watch(authFactorsProvider);
// Group settings into categories for better organization // Group settings into categories for better organization
final securitySettings = [ final securitySettings = [
ListTile( ListTile(
minLeadingWidth: 48, minLeadingWidth: 48,
title: Text('accountPassword').tr(), leading: const Icon(Symbols.devices),
subtitle: Text('accountPasswordDescription').tr().fontSize(12), title: Text('authSessions').tr(),
subtitle: Text('authSessionsDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.password),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
requestResetPassword(); showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => const AccountSessionSheet(),
);
}, },
), ),
ListTile( ExpansionTile(
minLeadingWidth: 48, leading: const Icon(
title: Text('accountTwoFactor').tr(), Symbols.security,
subtitle: Text('accountTwoFactorDescription').tr().fontSize(12), ).alignment(Alignment.centerLeft).width(48),
contentPadding: const EdgeInsets.only(left: 24, right: 17), title: Text('accountAuthFactor').tr(),
leading: const Icon(Symbols.security), subtitle: Text('accountAuthFactorDescription').tr().fontSize(12),
trailing: const Icon(Symbols.chevron_right), tilePadding: const EdgeInsets.only(left: 24, right: 17),
onTap: () { children: [
// Navigate to two-factor authentication settings authFactors.when(
showDialog( data:
context: context, (factors) => Column(
builder: children: [
(context) => AlertDialog( for (final factor in factors)
title: Text('accountTwoFactor').tr(), ListTile(
content: Text('accountTwoFactorSetupDescription').tr(), minLeadingWidth: 48,
actions: [ contentPadding: const EdgeInsets.only(
TextButton( left: 16,
onPressed: () => Navigator.of(context).pop(), right: 17,
child: Text('Close').tr(), top: 2,
), bottom: 4,
TextButton( ),
onPressed: () { title:
Navigator.of(context).pop(); Text(
// Add navigation to 2FA setup screen kFactorTypes[factor.type]!.$1,
style:
factor.enabledAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
).tr(),
subtitle:
Text(
kFactorTypes[factor.type]!.$2,
style:
factor.enabledAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
).tr(),
leading: CircleAvatar(
backgroundColor:
factor.enabledAt == null
? Theme.of(
context,
).colorScheme.secondaryContainer
: Theme.of(
context,
).colorScheme.primaryContainer,
child: Icon(kFactorTypes[factor.type]!.$3),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
isThreeLine: true,
onTap: () {
if (factor.type == 0) {
requestResetPassword();
return;
}
showModalBottomSheet(
context: context,
builder:
(context) => _AuthFactorSheet(factor: factor),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
}
});
},
),
if (factors.isNotEmpty) Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('authFactorNew').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => const _AuthFactorNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
}
});
}, },
child: Text('accountTwoFactorSetup').tr(),
), ),
], ],
), ),
); error:
}, (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(authFactorsProvider),
),
loading: () => ResponseLoadingWidget(),
),
],
), ),
]; ExpansionTile(
leading: const Icon(
final privacySettings = [ Symbols.contact_mail,
// ListTile( ).alignment(Alignment.centerLeft).width(48),
// minLeadingWidth: 48, title: Text('accountContactMethod').tr(),
// title: Text('accountPrivacy').tr(), subtitle: Text('accountContactMethodDescription').tr().fontSize(12),
// subtitle: Text('accountPrivacyDescription').tr().fontSize(12), tilePadding: const EdgeInsets.only(left: 24, right: 17),
// contentPadding: const EdgeInsets.only(left: 24, right: 17), children: [
// leading: const Icon(Symbols.visibility), ref
// trailing: const Icon(Symbols.chevron_right), .watch(contactMethodsProvider)
// onTap: () { .when(
// // Navigate to privacy settings data:
// }, (contacts) => Column(
// ), children: [
ListTile( for (final contact in contacts)
minLeadingWidth: 48, ListTile(
title: Text('accountDataExport').tr(), minLeadingWidth: 48,
subtitle: Text('accountDataExportDescription').tr().fontSize(12), contentPadding: const EdgeInsets.only(
contentPadding: const EdgeInsets.only(left: 24, right: 17), left: 16,
leading: const Icon(Symbols.download), right: 17,
trailing: const Icon(Symbols.chevron_right), top: 2,
onTap: () async { bottom: 4,
final confirm = await showConfirmAlert( ),
'accountDataExportConfirmation'.tr(), title: Text(
'accountDataExport'.tr(), contact.content,
); style:
if (!confirm || !context.mounted) return; contact.verifiedAt == null
// Add data export logic ? TextStyle(
showSnackBar(context, 'accountDataExportRequested'.tr()); decoration: TextDecoration.lineThrough,
}, )
: null,
),
subtitle: Text(
contact.type == 0
? 'contactMethodTypeEmail'.tr()
: 'contactMethodTypePhone'.tr(),
style:
contact.verifiedAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
),
leading: CircleAvatar(
backgroundColor:
contact.verifiedAt == null
? Theme.of(
context,
).colorScheme.secondaryContainer
: Theme.of(
context,
).colorScheme.primaryContainer,
child: Icon(
contact.type == 0
? Symbols.mail
: Symbols.phone,
),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
isThreeLine: false,
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) =>
_ContactMethodSheet(contact: contact),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
}
});
},
),
if (contacts.isNotEmpty) const Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('contactMethodNew').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) => const _ContactMethodNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
}
});
},
),
],
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(contactMethodsProvider),
),
loading: () => const ResponseLoadingWidget(),
),
],
), ),
]; ];
@ -172,10 +359,6 @@ class AccountSettingsScreen extends HookConsumerWidget {
title: 'accountSecurityTitle', title: 'accountSecurityTitle',
children: securitySettings, children: securitySettings,
), ),
_SettingsSection(
title: 'accountPrivacyTitle',
children: privacySettings,
),
], ],
), ),
), ),
@ -201,10 +384,6 @@ class AccountSettingsScreen extends HookConsumerWidget {
title: 'accountSecurityTitle', title: 'accountSecurityTitle',
children: securitySettings, children: securitySettings,
), ),
_SettingsSection(
title: 'accountPrivacyTitle',
children: privacySettings,
),
_SettingsSection( _SettingsSection(
title: 'accountDangerZoneTitle', title: 'accountDangerZoneTitle',
children: dangerZoneSettings, children: dangerZoneSettings,
@ -292,3 +471,599 @@ 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

@ -0,0 +1,48 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'settings.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$authFactorsHash() => r'4bb65bc0c065c4091c209ee81e57ddef41051ae2';
/// See also [authFactors].
@ProviderFor(authFactors)
final authFactorsProvider =
AutoDisposeFutureProvider<List<SnAuthFactor>>.internal(
authFactors,
name: r'authFactorsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$authFactorsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AuthFactorsRef = AutoDisposeFutureProviderRef<List<SnAuthFactor>>;
String _$contactMethodsHash() => r'4d7952fc196dce4dc646314565a49c115fd1d292';
/// See also [contactMethods].
@ProviderFor(contactMethods)
final contactMethodsProvider =
AutoDisposeFutureProvider<List<SnContactMethod>>.internal(
contactMethods,
name: r'contactMethodsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$contactMethodsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ContactMethodsRef = AutoDisposeFutureProviderRef<List<SnContactMethod>>;
// 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

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
@ -63,7 +64,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,

View File

@ -1,9 +1,16 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:island/models/auth.dart'; import 'package:island/models/auth.dart';
@ -24,12 +31,12 @@ import 'captcha.dart';
final Map<int, (String, String, IconData)> kFactorTypes = { final Map<int, (String, String, IconData)> kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), 0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), 1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), 2: (
3: (
'authFactorInAppNotify', 'authFactorInAppNotify',
'authFactorInAppNotifyDescription', 'authFactorInAppNotifyDescription',
Symbols.notifications_active, Symbols.notifications_active,
), ),
3: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
}; };
@RoutePage() @RoutePage()
@ -38,10 +45,13 @@ class LoginScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isBusy = useState(false);
final period = useState(0); final period = useState(0);
final currentTicket = useState<SnAuthChallenge?>(null); final currentTicket = useState<SnAuthChallenge?>(null);
final factors = useState<List<SnAuthFactor>>([]); final factors = useState<List<SnAuthFactor>>([]);
final factorPicked = useState<SnAuthFactor?>(null); final factorPicked = useState<SnAuthFactor?>(null);
return AppScaffold( return AppScaffold(
noBackground: false, noBackground: false,
appBar: AppBar( appBar: AppBar(
@ -50,54 +60,87 @@ class LoginScreen extends HookConsumerWidget {
), ),
body: Theme( body: Theme(
data: Theme.of(context).copyWith(canvasColor: Colors.transparent), data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
child: child: Column(
SingleChildScrollView( children: [
child: PageTransitionSwitcher( if (isBusy.value)
transitionBuilder: ( LinearProgressIndicator(
Widget child, minHeight: 4,
Animation<double> primaryAnimation, borderRadius: BorderRadius.zero,
Animation<double> secondaryAnimation, trackGap: 0,
) { stopIndicatorRadius: 0,
return SharedAxisTransition( )
animation: primaryAnimation, else if (currentTicket.value != null)
secondaryAnimation: secondaryAnimation, LinearProgressIndicator(
transitionType: SharedAxisTransitionType.horizontal, minHeight: 4,
child: Container( borderRadius: BorderRadius.zero,
constraints: BoxConstraints(maxWidth: 380), trackGap: 0,
child: child, stopIndicatorRadius: 0,
), value:
); 1 -
}, (currentTicket.value!.stepRemain /
child: switch (period.value % 3) { currentTicket.value!.stepTotal),
1 => _LoginPickerScreen( )
key: const ValueKey(1), else
ticket: currentTicket.value, const Gap(4),
factors: factors.value, Expanded(
onChallenge: child:
(SnAuthChallenge? p0) => currentTicket.value = p0, SingleChildScrollView(
onPickFactor: (SnAuthFactor p0) => factorPicked.value = p0, child: PageTransitionSwitcher(
onNext: () => period.value++, transitionBuilder: (
), Widget child,
2 => _LoginCheckScreen( Animation<double> primaryAnimation,
key: const ValueKey(2), Animation<double> secondaryAnimation,
challenge: currentTicket.value, ) {
factor: factorPicked.value, return SharedAxisTransition(
onChallenge: animation: primaryAnimation,
(SnAuthChallenge? p0) => currentTicket.value = p0, secondaryAnimation: secondaryAnimation,
onNext: () => period.value++, transitionType: SharedAxisTransitionType.horizontal,
), child: Container(
_ => _LoginLookupScreen( constraints: BoxConstraints(maxWidth: 380),
key: const ValueKey(0), child: child,
ticket: currentTicket.value, ),
onChallenge: );
(SnAuthChallenge? p0) => currentTicket.value = p0, },
onFactor: child: switch (period.value % 3) {
(List<SnAuthFactor>? p0) => factors.value = p0 ?? [], 1 => _LoginPickerScreen(
onNext: () => period.value++, key: const ValueKey(1),
), ticket: currentTicket.value,
}, factors: factors.value,
).padding(all: 24), onChallenge:
).center(), (SnAuthChallenge? p0) => currentTicket.value = p0,
onPickFactor:
(SnAuthFactor p0) => factorPicked.value = p0,
onNext: () => period.value++,
onBusy: (value) => isBusy.value = value,
),
2 => _LoginCheckScreen(
key: const ValueKey(2),
challenge: currentTicket.value,
factor: factorPicked.value,
onChallenge:
(SnAuthChallenge? p0) => currentTicket.value = p0,
onNext: () => period.value = 1,
onBusy: (value) => isBusy.value = value,
),
_ => _LoginLookupScreen(
key: const ValueKey(0),
ticket: currentTicket.value,
onChallenge:
(SnAuthChallenge? p0) => currentTicket.value = p0,
onFactor:
(List<SnAuthFactor>? p0) =>
factors.value = p0 ?? [],
onNext: () => period.value++,
onBusy: (value) => isBusy.value = value,
),
},
).padding(all: 24),
).center(),
),
const Gap(4),
],
),
), ),
); );
} }
@ -107,7 +150,8 @@ class _LoginCheckScreen extends HookConsumerWidget {
final SnAuthChallenge? challenge; final SnAuthChallenge? challenge;
final SnAuthFactor? factor; final SnAuthFactor? factor;
final Function(SnAuthChallenge?) onChallenge; final Function(SnAuthChallenge?) onChallenge;
final Function onNext; final VoidCallback onNext;
final Function(bool) onBusy;
const _LoginCheckScreen({ const _LoginCheckScreen({
super.key, super.key,
@ -115,6 +159,7 @@ class _LoginCheckScreen extends HookConsumerWidget {
required this.factor, required this.factor,
required this.onChallenge, required this.onChallenge,
required this.onNext, required this.onNext,
required this.onBusy,
}); });
@override @override
@ -122,11 +167,17 @@ class _LoginCheckScreen extends HookConsumerWidget {
final isBusy = useState(false); final isBusy = useState(false);
final passwordController = useTextEditingController(); final passwordController = useTextEditingController();
useEffect(() {
onBusy.call(isBusy.value);
return null;
}, [isBusy]);
Future<void> performCheckTicket() async { Future<void> performCheckTicket() async {
final pwd = passwordController.value.text; final pwd = passwordController.value.text;
if (pwd.isEmpty) return; if (pwd.isEmpty) return;
isBusy.value = true; isBusy.value = true;
try { try {
// Pass challenge
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final resp = await client.patch( final resp = await client.patch(
'/auth/challenge/${challenge!.id}', '/auth/challenge/${challenge!.id}',
@ -138,6 +189,8 @@ class _LoginCheckScreen extends HookConsumerWidget {
onNext(); onNext();
return; return;
} }
// Get token if challenge is completed
final tokenResp = await client.post( final tokenResp = await client.post(
'/auth/token', '/auth/token',
data: {'grant_type': 'authorization_code', 'code': result.id}, data: {'grant_type': 'authorization_code', 'code': result.id},
@ -146,6 +199,8 @@ class _LoginCheckScreen extends HookConsumerWidget {
setToken(ref.watch(sharedPreferencesProvider), token); setToken(ref.watch(sharedPreferencesProvider), token);
ref.invalidate(tokenProvider); ref.invalidate(tokenProvider);
if (!context.mounted) return; if (!context.mounted) return;
// Do post login tasks
final userNotifier = ref.read(userInfoProvider.notifier); final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser().then((_) { userNotifier.fetchUser().then((_) {
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
@ -154,6 +209,31 @@ class _LoginCheckScreen extends HookConsumerWidget {
wsNotifier.connect(); wsNotifier.connect();
if (context.mounted) Navigator.pop(context, true); 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),
);
}
}
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
return; return;
@ -162,6 +242,8 @@ class _LoginCheckScreen extends HookConsumerWidget {
} }
} }
final width = math.min(380, MediaQuery.of(context).size.width);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -176,24 +258,49 @@ class _LoginCheckScreen extends HookConsumerWidget {
'loginEnterPassword'.tr(), 'loginEnterPassword'.tr(),
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
).padding(left: 4, bottom: 16), ).padding(left: 4, bottom: 16),
TextField( if ([0].contains(factor!.type))
autocorrect: false, TextField(
enableSuggestions: false, autocorrect: false,
controller: passwordController, enableSuggestions: false,
obscureText: true, controller: passwordController,
autofillHints: [ obscureText: true,
factor!.type == 0 autofillHints: [
? AutofillHints.password factor!.type == 0
: AutofillHints.oneTimeCode, ? AutofillHints.password
], : AutofillHints.oneTimeCode,
decoration: InputDecoration( ],
isDense: true, decoration: InputDecoration(
border: const UnderlineInputBorder(), isDense: true,
labelText: 'password'.tr(), border: const OutlineInputBorder(),
labelText: 'password'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: isBusy.value ? null : (_) => performCheckTicket(),
).padding(horizontal: 7)
else
OtpTextField(
showCursor: false,
numberOfFields: 6,
obscureText: false,
showFieldAsBox: true,
focusedBorderColor: Theme.of(context).colorScheme.primary,
fieldWidth: (width / 6) - 10,
onSubmit: (value) {
passwordController.text = value;
performCheckTicket();
},
textStyle: Theme.of(context).textTheme.titleLarge!,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), const Gap(12),
onSubmitted: isBusy.value ? null : (_) => performCheckTicket(), Card(
).padding(horizontal: 7), 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(),
),
),
const Gap(12), const Gap(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -220,7 +327,8 @@ class _LoginPickerScreen extends HookConsumerWidget {
final List<SnAuthFactor>? factors; final List<SnAuthFactor>? factors;
final Function(SnAuthChallenge?) onChallenge; final Function(SnAuthChallenge?) onChallenge;
final Function(SnAuthFactor) onPickFactor; final Function(SnAuthFactor) onPickFactor;
final Function onNext; final VoidCallback onNext;
final Function(bool) onBusy;
const _LoginPickerScreen({ const _LoginPickerScreen({
super.key, super.key,
@ -229,17 +337,25 @@ class _LoginPickerScreen extends HookConsumerWidget {
required this.onChallenge, required this.onChallenge,
required this.onPickFactor, required this.onPickFactor,
required this.onNext, required this.onNext,
required this.onBusy,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isBusy = useState(false); final isBusy = useState(false);
final factorPicked = useState<String?>(null); final factorPicked = useState<SnAuthFactor?>(null);
useEffect(() {
onBusy.call(isBusy.value);
return null;
}, [isBusy]);
final unfocusColor = Theme.of( final unfocusColor = Theme.of(
context, context,
).colorScheme.onSurface.withAlpha((255 * 0.75).round()); ).colorScheme.onSurface.withAlpha((255 * 0.75).round());
final hintController = useTextEditingController();
void performGetFactorCode() async { void performGetFactorCode() async {
if (factorPicked.value == null) return; if (factorPicked.value == null) return;
@ -247,13 +363,24 @@ class _LoginPickerScreen extends HookConsumerWidget {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
try { try {
// Request one-time-password code
await client.post( await client.post(
'/auth/challenge/${ticket!.id}/factors/${factorPicked.value}', '/auth/challenge/${ticket!.id}/factors/${factorPicked.value!.id}',
data:
hintController.text.isNotEmpty
? jsonEncode(hintController.text)
: null,
); );
onPickFactor(factors!.where((x) => x.id == factorPicked.value).first); onPickFactor(factors!.where((x) => x == factorPicked.value).first);
onNext(); onNext();
} catch (err) { } catch (err) {
if (err is DioException && err.response?.statusCode == 400) {
onPickFactor(factors!.where((x) => x == factorPicked.value).first);
onNext();
if (context.mounted) {
showSnackBar(context, err.response!.data.toString());
}
return;
}
showErrorAlert(err); showErrorAlert(err);
return; return;
} finally { } finally {
@ -261,6 +388,20 @@ 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( return Column(
key: const ValueKey<int>(1), key: const ValueKey<int>(1),
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -292,10 +433,10 @@ class _LoginPickerScreen extends HookConsumerWidget {
), ),
title: Text(kFactorTypes[x.type]?.$1 ?? 'unknown').tr(), title: Text(kFactorTypes[x.type]?.$1 ?? 'unknown').tr(),
enabled: !ticket!.blacklistFactors.contains(x.id), enabled: !ticket!.blacklistFactors.contains(x.id),
value: factorPicked.value == x.id, value: factorPicked.value == x,
onChanged: (value) { onChanged: (value) {
if (value == true) { if (value == true) {
factorPicked.value = x.id; factorPicked.value = x;
} }
}, },
), ),
@ -304,6 +445,16 @@ class _LoginPickerScreen extends HookConsumerWidget {
List.empty(), List.empty(),
), ),
), ),
if ([1].contains(factorPicked.value?.type))
TextField(
controller: hintController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'authFactorHint'.tr(),
helperText: 'authFactorHintHelper'.tr(),
),
).padding(top: 12, bottom: 4, horizontal: 4),
const Gap(8), const Gap(8),
Text( Text(
'loginMultiFactor'.plural(ticket!.stepRemain), 'loginMultiFactor'.plural(ticket!.stepRemain),
@ -334,7 +485,8 @@ class _LoginLookupScreen extends HookConsumerWidget {
final SnAuthChallenge? ticket; final SnAuthChallenge? ticket;
final Function(SnAuthChallenge?) onChallenge; final Function(SnAuthChallenge?) onChallenge;
final Function(List<SnAuthFactor>?) onFactor; final Function(List<SnAuthFactor>?) onFactor;
final Function onNext; final VoidCallback onNext;
final Function(bool) onBusy;
const _LoginLookupScreen({ const _LoginLookupScreen({
super.key, super.key,
@ -342,6 +494,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
required this.onChallenge, required this.onChallenge,
required this.onFactor, required this.onFactor,
required this.onNext, required this.onNext,
required this.onBusy,
}); });
@override @override
@ -349,6 +502,11 @@ class _LoginLookupScreen extends HookConsumerWidget {
final isBusy = useState(false); final isBusy = useState(false);
final usernameController = useTextEditingController(); final usernameController = useTextEditingController();
useEffect(() {
onBusy.call(isBusy.value);
return null;
}, [isBusy]);
Future<void> requestResetPassword() async { Future<void> requestResetPassword() async {
final uname = usernameController.value.text; final uname = usernameController.value.text;
if (uname.isEmpty) { if (uname.isEmpty) {

View File

@ -24,6 +24,7 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_overlay.dart'; import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/realms/selection_dropdown.dart'; import 'package:island/widgets/realms/selection_dropdown.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -346,7 +347,11 @@ class ChatListScreen extends HookConsumerWidget {
builder: (context, ref, _) { builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider); final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen( return summaryState.maybeWhen(
loading: () => const LinearProgressIndicator(), loading:
() => const LinearProgressIndicator(
minHeight: 2,
borderRadius: BorderRadius.zero,
),
orElse: () => const SizedBox.shrink(), orElse: () => const SizedBox.shrink(),
); );
}, },
@ -527,7 +532,10 @@ class EditChatScreen extends HookConsumerWidget {
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
@ -711,109 +719,77 @@ class _ChatInvitesSheet extends HookConsumerWidget {
} }
} }
return Container( return SheetScaffold(
constraints: BoxConstraints( titleText: 'invites'.tr(),
maxHeight: MediaQuery.of(context).size.height * 0.8, actions: [
), IconButton(
child: Column( icon: const Icon(Symbols.refresh),
mainAxisSize: MainAxisSize.min, style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
children: [ onPressed: () {
Padding( ref.invalidate(realmInvitesProvider);
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), },
child: Row( ),
children: [ ],
Text( child: invites.when(
'invites'.tr(), data:
style: Theme.of(context).textTheme.headlineSmall?.copyWith( (items) =>
fontWeight: FontWeight.w600, items.isEmpty
letterSpacing: -0.5, ? Center(
), child:
), Text(
const Spacer(), 'invitesEmpty',
IconButton( textAlign: TextAlign.center,
icon: const Icon(Symbols.refresh), ).tr(),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)), )
onPressed: () { : ListView.builder(
ref.invalidate(chatroomInvitesProvider); shrinkWrap: true,
}, itemCount: items.length,
), itemBuilder: (context, index) {
IconButton( final invite = items[index];
icon: const Icon(Symbols.close), return ChatRoomListTile(
onPressed: () => Navigator.pop(context), room: invite.chatRoom!,
style: IconButton.styleFrom(minimumSize: const Size(36, 36)), isDirect: invite.chatRoom!.type == 1,
), subtitle: Row(
], spacing: 6,
), children: [
), Flexible(
const Divider(height: 1), child:
Expanded( Text(
child: invites.when( invite.role >= 100
data: ? 'permissionOwner'
(items) => : invite.role >= 50
items.isEmpty ? 'permissionModerator'
? Center( : 'permissionMember',
child: ).tr(),
Text( ),
'invitesEmpty', if (invite.chatRoom!.type == 1)
textAlign: TextAlign.center, Badge(
).tr(), label: Text('directMessage').tr(),
) backgroundColor:
: ListView.builder( Theme.of(context).colorScheme.primary,
shrinkWrap: true, textColor:
itemCount: items.length, Theme.of(context).colorScheme.onPrimary,
itemBuilder: (context, index) {
final invite = items[index];
return ChatRoomListTile(
room: invite.chatRoom!,
isDirect: invite.chatRoom!.type == 1,
subtitle: Row(
spacing: 6,
children: [
Flexible(
child:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
),
if (invite.chatRoom!.type == 1)
Badge(
label: Text('directMessage').tr(),
backgroundColor:
Theme.of(
context,
).colorScheme.primary,
textColor:
Theme.of(
context,
).colorScheme.onPrimary,
),
],
), ),
trailing: Row( ],
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
), ),
loading: () => const Center(child: CircularProgressIndicator()), trailing: Row(
error: (error, stack) => Center(child: Text('Error: $error')), mainAxisSize: MainAxisSize.min,
), children: [
), IconButton(
], icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
), ),
); );
} }

View File

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -17,12 +19,12 @@ import 'package:island/pods/database.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_overlay.dart'; import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/chat/message_item.dart'; import 'package:island/widgets/chat/message_item.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
@ -319,6 +321,46 @@ class ChatRoomScreen extends HookConsumerWidget {
); );
} }
// Members who are typing
final typingStatuses = useState<List<SnChatMember>>([]);
final typingDebouncer = useState<Timer?>(null);
void sendTypingStatus() {
// Don't send if we're already in a cooldown period
if (typingDebouncer.value != null) return;
// Send typing status immediately
final wsState = ref.read(websocketStateProvider.notifier);
wsState.sendMessage(
jsonEncode(
WebSocketPacket(type: 'messages.typing', data: {'chat_room_id': id}),
),
);
typingDebouncer.value = Timer(const Duration(milliseconds: 1000), () {
typingDebouncer.value = null;
});
}
// Add timer to remove typing status after inactivity
useEffect(() {
final removeTypingTimer = Timer.periodic(const Duration(seconds: 5), (_) {
if (typingStatuses.value.isNotEmpty) {
// Remove typing statuses older than 5 seconds
final now = DateTime.now();
typingStatuses.value =
typingStatuses.value.where((member) {
final lastTyped =
member.lastTyped ??
DateTime.now().subtract(const Duration(milliseconds: 1350));
return now.difference(lastTyped).inSeconds < 5;
}).toList();
}
});
return () => removeTypingTimer.cancel();
}, []);
var isLoading = false; var isLoading = false;
// Add scroll listener for pagination // Add scroll listener for pagination
@ -341,6 +383,28 @@ class ChatRoomScreen extends HookConsumerWidget {
void onMessage(WebSocketPacket pkt) { void onMessage(WebSocketPacket pkt) {
if (!pkt.type.startsWith('messages')) return; if (!pkt.type.startsWith('messages')) return;
if (['messages.read'].contains(pkt.type)) return; if (['messages.read'].contains(pkt.type)) return;
if (pkt.type == 'messages.typing') {
final sender = SnChatMember.fromJson(
pkt.data!['sender'],
).copyWith(lastTyped: DateTime.now());
// Check if the sender is already in the typing list
final existingIndex = typingStatuses.value.indexWhere(
(member) => member.id == sender.id,
);
if (existingIndex >= 0) {
// Update the existing entry with new timestamp
final updatedList = [...typingStatuses.value];
updatedList[existingIndex] = sender;
typingStatuses.value = updatedList;
} else {
// Add new typing status
typingStatuses.value = [...typingStatuses.value, sender];
}
return;
}
final message = SnChatMessage.fromJson(pkt.data!); final message = SnChatMessage.fromJson(pkt.data!);
if (message.chatRoomId != chatRoom.value?.id) return; if (message.chatRoomId != chatRoom.value?.id) return;
switch (pkt.type) { switch (pkt.type) {
@ -414,8 +478,22 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
} }
// Add listener to message controller for typing status
useEffect(() {
void onTextChange() {
if (messageController.text.isNotEmpty) {
sendTypingStatus();
}
}
messageController.addListener(onTextChange);
return () => messageController.removeListener(onTextChange);
}, [messageController]);
final compactHeader = isWideScreen(context); final compactHeader = isWideScreen(context);
final listController = useMemoized(() => ListController(), []);
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: !compactHeader ? const Center(child: PageBackButton()) : null, leading: !compactHeader ? const Center(child: PageBackButton()) : null,
@ -514,7 +592,7 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
loading: () => const Text('Loading...'), loading: () => const Text('Loading...'),
error: error:
(err, __) => ResponseErrorWidget( (err, _) => ResponseErrorWidget(
error: err, error: err,
onRetry: () => messagesNotifier.loadInitial(), onRetry: () => messagesNotifier.loadInitial(),
), ),
@ -541,11 +619,19 @@ class ChatRoomScreen extends HookConsumerWidget {
messageList.isEmpty messageList.isEmpty
? Center(child: Text('No messages yet'.tr())) ? Center(child: Text('No messages yet'.tr()))
: SuperListView.builder( : SuperListView.builder(
listController: listController,
padding: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController, controller: scrollController,
reverse: reverse:
true, // Show newest messages at the bottom true, // Show newest messages at the bottom
itemCount: messageList.length, itemCount: messageList.length,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey;
final messageId = valueKey.value as String;
return messageList.indexWhere(
(m) => m.id == messageId,
);
},
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = messageList[index]; final message = messageList[index];
final nextMessage = final nextMessage =
@ -602,6 +688,18 @@ class ChatRoomScreen extends HookConsumerWidget {
message.toRemoteMessage(); message.toRemoteMessage();
} }
}, },
onJump: (messageId) {
final messageIndex = messageList
.indexWhere(
(m) => m.id == messageId,
);
listController.jumpToItem(
index: messageIndex,
scrollController:
scrollController,
alignment: 0.5,
);
},
progress: progress:
attachmentProgress.value[message attachmentProgress.value[message
.id], .id],
@ -614,8 +712,9 @@ class ChatRoomScreen extends HookConsumerWidget {
onAction: null, onAction: null,
progress: null, progress: null,
showAvatar: false, showAvatar: false,
onJump: (_) {},
), ),
error: (_, __) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
); );
}, },
), ),
@ -630,57 +729,135 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
chatRoom.when( chatRoom.when(
data: data:
(room) => _ChatInput( (room) => Column(
messageController: messageController, mainAxisSize: MainAxisSize.min,
chatRoom: room!, children: [
onSend: sendMessage, AnimatedSwitcher(
onClear: () { duration: const Duration(milliseconds: 300),
if (messageEditingTo.value != null) { switchInCurve: Curves.fastEaseInToSlowEaseOut,
attachments.value.clear(); switchOutCurve: Curves.fastEaseInToSlowEaseOut,
messageController.clear(); transitionBuilder: (
} Widget child,
messageEditingTo.value = null; Animation<double> animation,
messageReplyingTo.value = null; ) {
messageForwardingTo.value = null; return SlideTransition(
}, position: Tween<Offset>(
messageEditingTo: messageEditingTo.value, begin: const Offset(0, -0.3),
messageReplyingTo: messageReplyingTo.value, end: Offset.zero,
messageForwardingTo: messageForwardingTo.value, ).animate(
onPickFile: (bool isPhoto) { CurvedAnimation(
if (isPhoto) { parent: animation,
pickPhotoMedia(); curve: Curves.easeOutCubic,
} else { ),
pickVideoMedia(); ),
} child: SizeTransition(
}, sizeFactor: animation,
attachments: attachments.value, axisAlignment: -1.0,
onUploadAttachment: (_) { child: FadeTransition(
// not going to do anything, only upload when send the message opacity: animation,
}, child: child,
onDeleteAttachment: (index) async { ),
final attachment = attachments.value[index]; ),
if (attachment.isOnCloud) { );
final client = ref.watch(apiClientProvider); },
await client.delete('/files/${attachment.data.id}'); child:
} typingStatuses.value.isNotEmpty
final clone = List.of(attachments.value); ? Container(
clone.removeAt(index); key: const ValueKey('typing-indicator'),
attachments.value = clone; width: double.infinity,
}, padding: const EdgeInsets.symmetric(
onMoveAttachment: (idx, delta) { horizontal: 16,
if (idx + delta < 0 || vertical: 4,
idx + delta >= attachments.value.length) { ),
return; child: Row(
} children: [
final clone = List.of(attachments.value); const Icon(
clone.insert(idx + delta, clone.removeAt(idx)); Symbols.more_horiz,
attachments.value = clone; size: 16,
}, ).padding(horizontal: 8),
onAttachmentsChanged: (newAttachments) { const Gap(8),
attachments.value = newAttachments; Expanded(
}, child: Text(
'typingHint'.plural(
typingStatuses.value.length,
args: [
typingStatuses.value
.map(
(x) =>
x.nick ??
x.account.nick,
)
.join(', '),
],
),
style:
Theme.of(
context,
).textTheme.bodySmall,
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('no_typing'),
),
),
_ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
attachments: attachments.value,
onUploadAttachment: (_) {
// not going to do anything, only upload when send the message
},
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
),
],
), ),
error: (_, __) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
), ),
], ],
@ -697,7 +874,7 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
} }
class _ChatInput extends ConsumerWidget { class _ChatInput extends HookConsumerWidget {
final TextEditingController messageController; final TextEditingController messageController;
final SnChatRoom chatRoom; final SnChatRoom chatRoom;
final VoidCallback onSend; final VoidCallback onSend;
@ -728,46 +905,59 @@ class _ChatInput extends ConsumerWidget {
required this.onAttachmentsChanged, required this.onAttachmentsChanged,
}); });
void _handleKeyPress(BuildContext context, WidgetRef ref, RawKeyEvent event) {
if (event is! RawKeyDownEvent) return;
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
if (isPaste && isModifierPressed) {
_handlePaste();
return;
}
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
if (isEnter) {
if (enterToSend && !isModifierPressed) {
onSend();
} else if (!enterToSend && isModifierPressed) {
onSend();
}
}
}
Future<void> _handlePaste() async {
final clipboard = await Pasteboard.image;
if (clipboard == null) return;
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
type: UniversalFileType.image,
),
]);
}
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final inputFocusNode = useFocusNode();
final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend; final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend;
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
void send() {
inputFocusNode.requestFocus();
onSend.call();
}
Future<void> handlePaste() async {
final clipboard = await Pasteboard.image;
if (clipboard == null) return;
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
type: UniversalFileType.image,
),
]);
}
void handleKeyPress(
BuildContext context,
WidgetRef ref,
RawKeyEvent event,
) {
if (event is! RawKeyDownEvent) return;
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
if (isPaste && isModifierPressed) {
handlePaste();
return;
}
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
if (isEnter) {
if (enterToSend && !isModifierPressed) {
send();
} else if (!enterToSend && isModifierPressed) {
send();
}
}
}
return Material( return Material(
elevation: 8, elevation: 8,
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
@ -788,7 +978,7 @@ class _ChatInput extends ConsumerWidget {
onMove: (delta) => onMoveAttachment(idx, delta), onMove: (delta) => onMoveAttachment(idx, delta),
); );
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, _) => const Gap(8),
), ),
).padding(top: 12), ).padding(top: 12),
if (messageReplyingTo != null || if (messageReplyingTo != null ||
@ -869,12 +1059,23 @@ class _ChatInput extends ConsumerWidget {
Expanded( Expanded(
child: RawKeyboardListener( child: RawKeyboardListener(
focusNode: FocusNode(), focusNode: FocusNode(),
onKey: (event) => _handleKeyPress(context, ref, event), onKey: (event) => handleKeyPress(context, ref, event),
child: TextField( child: TextField(
focusNode: inputFocusNode,
controller: messageController, controller: messageController,
onSubmitted: enterToSend ? (_) => onSend() : null, onSubmitted:
(enterToSend && isMobile)
? (_) {
send();
}
: null,
keyboardType:
(enterToSend && isMobile)
? TextInputType.text
: TextInputType.multiline,
textInputAction: TextInputAction.send,
inputFormatters: [ inputFormatters: [
if (enterToSend) if (enterToSend && !isMobile)
TextInputFormatter.withFunction((oldValue, newValue) { TextInputFormatter.withFunction((oldValue, newValue) {
if (newValue.text.endsWith('\n')) { if (newValue.text.endsWith('\n')) {
return oldValue; return oldValue;
@ -909,7 +1110,7 @@ class _ChatInput extends ConsumerWidget {
IconButton( IconButton(
icon: const Icon(Icons.send), icon: const Icon(Icons.send),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: onSend, onPressed: send,
), ),
], ],
).padding(bottom: MediaQuery.of(context).padding.bottom), ).padding(bottom: MediaQuery.of(context).padding.bottom),

View File

@ -14,10 +14,14 @@ import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/paging_helper_ext.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'room_detail.freezed.dart'; part 'room_detail.freezed.dart';
part 'room_detail.g.dart';
@RoutePage() @RoutePage()
class ChatDetailScreen extends HookConsumerWidget { class ChatDetailScreen extends HookConsumerWidget {
@ -287,12 +291,51 @@ class ChatMemberNotifier extends StateNotifier<ChatRoomMemberState> {
} }
} }
@riverpod
class ChatMemberListNotifier extends _$ChatMemberListNotifier
with CursorPagingNotifierMixin<SnChatMember> {
@override
Future<CursorPagingData<SnChatMember>> build(String roomId) {
return fetch();
}
@override
Future<CursorPagingData<SnChatMember>> fetch({String? cursor}) async {
final offset = cursor == null ? 0 : int.parse(cursor);
final take = 20;
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get(
'/chat/$roomId/members',
queryParameters: {'offset': offset, 'take': take},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnChatMember.fromJson(e)).toList();
// Calculate next cursor based on total count
final nextOffset = offset + members.length;
final String? nextCursor =
nextOffset < total ? nextOffset.toString() : null;
return CursorPagingData(
items: members,
nextCursor: nextCursor,
hasMore: members.length < total,
);
}
}
class _ChatMemberListSheet extends HookConsumerWidget { class _ChatMemberListSheet extends HookConsumerWidget {
final String roomId; final String roomId;
const _ChatMemberListSheet({required this.roomId}); const _ChatMemberListSheet({required this.roomId});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final memberListProvider = chatMemberListNotifierProvider(roomId);
// For backward compatibility and to show total count in the header
final memberState = ref.watch(chatMemberStateProvider(roomId)); final memberState = ref.watch(chatMemberStateProvider(roomId));
final memberNotifier = ref.read(chatMemberStateProvider(roomId).notifier); final memberNotifier = ref.read(chatMemberStateProvider(roomId).notifier);
@ -318,8 +361,10 @@ class _ChatMemberListSheet extends HookConsumerWidget {
'/chat/invites/$roomId', '/chat/invites/$roomId',
data: {'related_user_id': result.id, 'role': 0}, data: {'related_user_id': result.id, 'role': 0},
); );
// Refresh both providers
memberNotifier.reset(); memberNotifier.reset();
await memberNotifier.loadMore(); await memberNotifier.loadMore();
ref.invalidate(memberListProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
} }
@ -351,8 +396,10 @@ class _ChatMemberListSheet extends HookConsumerWidget {
IconButton( IconButton(
icon: const Icon(Symbols.refresh), icon: const Icon(Symbols.refresh),
onPressed: () { onPressed: () {
// Refresh both providers
memberNotifier.reset(); memberNotifier.reset();
memberNotifier.loadMore(); memberNotifier.loadMore();
ref.invalidate(memberListProvider);
}, },
), ),
IconButton( IconButton(
@ -365,108 +412,103 @@ class _ChatMemberListSheet extends HookConsumerWidget {
), ),
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: child: PagingHelperView(
memberState.error != null provider: memberListProvider,
? Center(child: Text(memberState.error!)) futureRefreshable: memberListProvider.future,
: ListView.builder( notifierRefreshable: memberListProvider.notifier,
itemCount: memberState.members.length + 1, contentBuilder: (data, widgetCount, endItemView) {
itemBuilder: (context, index) { return ListView.builder(
if (index == memberState.members.length) { itemCount: widgetCount,
if (memberState.isLoading) { itemBuilder: (context, index) {
return const Center( if (index == data.items.length) {
child: Padding( return endItemView;
padding: EdgeInsets.all(16.0), }
child: CircularProgressIndicator(),
),
);
}
if (memberState.members.length < memberState.total) {
memberNotifier.loadMore(
offset: memberState.members.length,
);
}
return const SizedBox.shrink();
}
final member = memberState.members[index]; final member = data.items[index];
return ListTile( return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12), contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
fileId: member.account.profile.picture?.id, fileId: member.account.profile.picture?.id,
), ),
title: Row( title: Row(
spacing: 6, spacing: 6,
children: [ children: [
Flexible(child: Text(member.account.nick)), Flexible(child: Text(member.account.nick)),
if (member.joinedAt == null) if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20), const Icon(Symbols.pending_actions, size: 20),
], ],
), ),
subtitle: Row( subtitle: Row(
children: [ children: [
Text( Text(
member.role >= 100 member.role >= 100
? 'permissionOwner' ? 'permissionOwner'
: member.role >= 50 : member.role >= 50
? 'permissionModerator' ? 'permissionModerator'
: 'permissionMember', : 'permissionMember',
).tr(), ).tr(),
Text('·').bold().padding(horizontal: 6), Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account.name}")), Expanded(child: Text("@${member.account.name}")),
], ],
), ),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if ((roomIdentity.value?.role ?? 0) >= 50) if ((roomIdentity.value?.role ?? 0) >= 50)
IconButton( IconButton(
icon: const Icon(Symbols.edit), icon: const Icon(Symbols.edit),
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: builder:
(context) => _ChatMemberRoleSheet( (context) => _ChatMemberRoleSheet(
roomId: roomId, roomId: roomId,
member: member, member: member,
), ),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
memberNotifier.reset(); // Refresh both providers
memberNotifier.loadMore(); memberNotifier.reset();
} memberNotifier.loadMore();
}); ref.invalidate(memberListProvider);
}, }
), });
if ((roomIdentity.value?.role ?? 0) >= 50) },
IconButton( ),
icon: const Icon(Symbols.delete), if ((roomIdentity.value?.role ?? 0) >= 50)
onPressed: () { IconButton(
showConfirmAlert( icon: const Icon(Symbols.delete),
'removeChatMemberHint'.tr(), onPressed: () {
'removeChatMember'.tr(), showConfirmAlert(
).then((confirm) async { 'removeChatMemberHint'.tr(),
if (confirm != true) return; 'removeChatMember'.tr(),
try { ).then((confirm) async {
final apiClient = ref.watch( if (confirm != true) return;
apiClientProvider, try {
); final apiClient = ref.watch(
await apiClient.delete( apiClientProvider,
'/chat/$roomId/members/${member.accountId}', );
); await apiClient.delete(
memberNotifier.reset(); '/chat/$roomId/members/${member.accountId}',
memberNotifier.loadMore(); );
} catch (err) { // Refresh both providers
showErrorAlert(err); memberNotifier.reset();
} memberNotifier.loadMore();
}); ref.invalidate(memberListProvider);
}, } catch (err) {
), showErrorAlert(err);
], }
), });
); },
}, ),
), ],
),
);
},
);
},
),
), ),
], ],
), ),

View File

@ -0,0 +1,180 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'room_detail.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$chatMemberListNotifierHash() =>
r'f2191a631ba00ae3de39ccac10e4cdd065ffee17';
/// 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 _$ChatMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> {
late final String roomId;
FutureOr<CursorPagingData<SnChatMember>> build(String roomId);
}
/// See also [ChatMemberListNotifier].
@ProviderFor(ChatMemberListNotifier)
const chatMemberListNotifierProvider = ChatMemberListNotifierFamily();
/// See also [ChatMemberListNotifier].
class ChatMemberListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnChatMember>>> {
/// See also [ChatMemberListNotifier].
const ChatMemberListNotifierFamily();
/// See also [ChatMemberListNotifier].
ChatMemberListNotifierProvider call(String roomId) {
return ChatMemberListNotifierProvider(roomId);
}
@override
ChatMemberListNotifierProvider getProviderOverride(
covariant ChatMemberListNotifierProvider provider,
) {
return call(provider.roomId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'chatMemberListNotifierProvider';
}
/// See also [ChatMemberListNotifier].
class ChatMemberListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ChatMemberListNotifier,
CursorPagingData<SnChatMember>
> {
/// See also [ChatMemberListNotifier].
ChatMemberListNotifierProvider(String roomId)
: this._internal(
() => ChatMemberListNotifier()..roomId = roomId,
from: chatMemberListNotifierProvider,
name: r'chatMemberListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$chatMemberListNotifierHash,
dependencies: ChatMemberListNotifierFamily._dependencies,
allTransitiveDependencies:
ChatMemberListNotifierFamily._allTransitiveDependencies,
roomId: roomId,
);
ChatMemberListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.roomId,
}) : super.internal();
final String roomId;
@override
FutureOr<CursorPagingData<SnChatMember>> runNotifierBuild(
covariant ChatMemberListNotifier notifier,
) {
return notifier.build(roomId);
}
@override
Override overrideWith(ChatMemberListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ChatMemberListNotifierProvider._internal(
() => create()..roomId = roomId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
roomId: roomId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ChatMemberListNotifier,
CursorPagingData<SnChatMember>
>
createElement() {
return _ChatMemberListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ChatMemberListNotifierProvider && other.roomId == roomId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, roomId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ChatMemberListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnChatMember>> {
/// The parameter `roomId` of this provider.
String get roomId;
}
class _ChatMemberListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ChatMemberListNotifier,
CursorPagingData<SnChatMember>
>
with ChatMemberListNotifierRef {
_ChatMemberListNotifierProviderElement(super.provider);
@override
String get roomId => (origin as ChatMemberListNotifierProvider).roomId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
@ -99,7 +100,10 @@ class EditPublisherScreen extends HookConsumerWidget {
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,

View File

@ -298,7 +298,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
if (confirm) { if (confirm) {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
client.delete('/stickers/$packId'); client.delete('/stickers/$packId');
ref.invalidate(stickerPacksProvider); ref.invalidate(stickerPacksNotifierProvider);
if (context.mounted) context.router.maybePop(true); if (context.mounted) context.router.maybePop(true);
} }
}); });

View File

@ -13,7 +13,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'stickers.g.dart'; part 'stickers.g.dart';
@ -24,9 +24,6 @@ class StickersScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final stickersState = ref.watch(stickerPacksProvider);
final stickersNotifier = ref.watch(stickerPacksProvider.notifier);
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('stickers').tr(), title: const Text('stickers').tr(),
@ -37,7 +34,7 @@ class StickersScreen extends HookConsumerWidget {
value, value,
) { ) {
if (value != null) { if (value != null) {
stickersNotifier.refresh(); ref.invalidate(stickerPacksNotifierProvider(pubName));
} }
}); });
}, },
@ -46,104 +43,90 @@ class StickersScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
body: stickersState.when( body: SliverStickerPacksList(pubName: pubName),
data:
(stickers) => RefreshIndicator(
onRefresh: stickersNotifier.refresh,
child: InfiniteList(
padding: EdgeInsets.zero,
itemCount: stickers.length,
hasReachedMax: stickersNotifier.isReachedMax,
isLoading: stickersNotifier.isLoading,
onFetchData: stickersNotifier.fetchMore,
itemBuilder: (context, index) {
return ListTile(
title: Text(stickers[index].name),
subtitle: Text(stickers[index].description),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.router.push(
StickerPackDetailRoute(
pubName: pubName,
id: stickers[index].id,
),
);
},
);
},
),
),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
),
); );
} }
} }
final stickerPacksProvider = StateNotifierProvider< class SliverStickerPacksList extends HookConsumerWidget {
StickerPacksNotifier, final String pubName;
AsyncValue<List<SnStickerPack>> const SliverStickerPacksList({super.key, required this.pubName});
>((ref) {
return StickerPacksNotifier(ref.watch(apiClientProvider));
});
class StickerPacksNotifier @override
extends StateNotifier<AsyncValue<List<SnStickerPack>>> { Widget build(BuildContext context, WidgetRef ref) {
final Dio _apiClient; return PagingHelperView(
StickerPacksNotifier(this._apiClient) : super(const AsyncValue.loading()) { provider: stickerPacksNotifierProvider(pubName),
fetchStickers(); futureRefreshable: stickerPacksNotifierProvider(pubName).future,
notifierRefreshable: stickerPacksNotifierProvider(pubName).notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final sticker = data.items[index];
return ListTile(
title: Text(sticker.name),
subtitle: Text(sticker.description),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.router.push(
StickerPackDetailRoute(pubName: pubName, id: sticker.id),
);
},
);
},
),
);
}
}
@riverpod
class StickerPacksNotifier extends _$StickerPacksNotifier
with CursorPagingNotifierMixin<SnStickerPack> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnStickerPack>> build(String pubName) {
return fetch(cursor: null);
} }
int offset = 0; @override
int take = 20; Future<CursorPagingData<SnStickerPack>> fetch({
int total = 0; required String? cursor,
}) async {
bool isLoading = false; final client = ref.read(apiClientProvider);
bool get isReachedMax => final offset = cursor == null ? 0 : int.parse(cursor);
state.valueOrNull != null && state.valueOrNull!.length >= total;
Future<void> fetchStickers() async {
if (isLoading) return;
isLoading = true;
try { try {
final response = await _apiClient.get( final response = await client.get(
'/stickers?offset=$offset&take=$take', '/stickers',
queryParameters: {
'offset': offset,
'take': _pageSize,
'pubName': pubName,
},
); );
if (response.statusCode == 200) {
total = int.parse(response.headers.value('X-Total') ?? '0');
final newStickers =
response.data
.map((e) => SnStickerPack.fromJson(e))
.cast<SnStickerPack>()
.toList();
state = AsyncValue.data( final total = int.parse(response.headers.value('X-Total') ?? '0');
state.valueOrNull != null final List<dynamic> data = response.data;
? [...state.value!, ...newStickers] final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList();
: newStickers,
); final hasMore = offset + stickers.length < total;
offset += take; final nextCursor = hasMore ? (offset + stickers.length).toString() : null;
} else {
state = AsyncValue.error('Failed to load stickers', StackTrace.current); return CursorPagingData(
} items: stickers,
} catch (err, stackTrace) { hasMore: hasMore,
state = AsyncValue.error(err, stackTrace); nextCursor: nextCursor,
} finally { );
isLoading = false; } catch (err) {
rethrow;
} }
} }
Future<void> fetchMore() async {
if (state.valueOrNull == null || state.valueOrNull!.length >= total) return;
await fetchStickers();
}
Future<void> refresh() async {
offset = 0;
state = const AsyncValue.loading();
await fetchStickers();
}
} }
@riverpod @riverpod

View File

@ -147,5 +147,154 @@ class _StickerPackProviderElement
String? get packId => (origin as StickerPackProvider).packId; String? get packId => (origin as StickerPackProvider).packId;
} }
String _$stickerPacksNotifierHash() =>
r'dc0cc4ec27fdd6d5da28f982ff10c852f8107a18';
abstract class _$StickerPacksNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> {
late final String pubName;
FutureOr<CursorPagingData<SnStickerPack>> build(String pubName);
}
/// See also [StickerPacksNotifier].
@ProviderFor(StickerPacksNotifier)
const stickerPacksNotifierProvider = StickerPacksNotifierFamily();
/// See also [StickerPacksNotifier].
class StickerPacksNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnStickerPack>>> {
/// See also [StickerPacksNotifier].
const StickerPacksNotifierFamily();
/// See also [StickerPacksNotifier].
StickerPacksNotifierProvider call(String pubName) {
return StickerPacksNotifierProvider(pubName);
}
@override
StickerPacksNotifierProvider getProviderOverride(
covariant StickerPacksNotifierProvider 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'stickerPacksNotifierProvider';
}
/// See also [StickerPacksNotifier].
class StickerPacksNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
StickerPacksNotifier,
CursorPagingData<SnStickerPack>
> {
/// See also [StickerPacksNotifier].
StickerPacksNotifierProvider(String pubName)
: this._internal(
() => StickerPacksNotifier()..pubName = pubName,
from: stickerPacksNotifierProvider,
name: r'stickerPacksNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$stickerPacksNotifierHash,
dependencies: StickerPacksNotifierFamily._dependencies,
allTransitiveDependencies:
StickerPacksNotifierFamily._allTransitiveDependencies,
pubName: pubName,
);
StickerPacksNotifierProvider._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
FutureOr<CursorPagingData<SnStickerPack>> runNotifierBuild(
covariant StickerPacksNotifier notifier,
) {
return notifier.build(pubName);
}
@override
Override overrideWith(StickerPacksNotifier Function() create) {
return ProviderOverride(
origin: this,
override: StickerPacksNotifierProvider._internal(
() => create()..pubName = pubName,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
StickerPacksNotifier,
CursorPagingData<SnStickerPack>
>
createElement() {
return _StickerPacksNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is StickerPacksNotifierProvider && 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 StickerPacksNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnStickerPack>> {
/// The parameter `pubName` of this provider.
String get pubName;
}
class _StickerPacksNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
StickerPacksNotifier,
CursorPagingData<SnStickerPack>
>
with StickerPacksNotifierRef {
_StickerPacksNotifierProviderElement(super.provider);
@override
String get pubName => (origin as StickerPacksNotifierProvider).pubName;
}
// ignore_for_file: type=lint // 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 // 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

@ -168,12 +168,6 @@ class _ActivityListView extends HookConsumerWidget {
); );
} }
break; break;
case 'accounts.check-in':
itemWidget = CheckInActivityWidget(item: item);
break;
case 'accounts.status':
itemWidget = StatusActivityWidget(item: item);
break;
default: default:
itemWidget = const Placeholder(); itemWidget = const Placeholder();
} }
@ -196,12 +190,14 @@ class ActivityListNotifier extends _$ActivityListNotifier
@override @override
Future<CursorPagingData<SnActivity>> fetch({required String? cursor}) async { Future<CursorPagingData<SnActivity>> fetch({required String? cursor}) async {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final take = 20; final take = 20;
final response = await client.get( final response = await client.get(
'/activities', '/activities',
queryParameters: {'offset': offset, 'take': take}, queryParameters: {
if (cursor != null) 'reading_cursor': cursor,
'take': take,
},
); );
final List<SnActivity> items = final List<SnActivity> items =
@ -209,9 +205,9 @@ class ActivityListNotifier extends _$ActivityListNotifier
.map((e) => SnActivity.fromJson(e as Map<String, dynamic>)) .map((e) => SnActivity.fromJson(e as Map<String, dynamic>))
.toList(); .toList();
final total = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0; final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
final hasMore = offset + items.length < total; final nextCursor =
final nextCursor = hasMore ? (offset + items.length).toString() : null; items.map((x) => x.createdAt).lastOrNull?.toIso8601String().toString();
return CursorPagingData( return CursorPagingData(
items: items, items: items,

View File

@ -7,7 +7,7 @@ part of 'explore.dart';
// ************************************************************************** // **************************************************************************
String _$activityListNotifierHash() => String _$activityListNotifierHash() =>
r'8a67d302e828408c7c4cf724d84c2c5958f2dc7e'; r'1baf0bb961bc02bfc8a5b5f515981072c6ce1750';
/// See also [ActivityListNotifier]. /// See also [ActivityListNotifier].
@ProviderFor(ActivityListNotifier) @ProviderFor(ActivityListNotifier)

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@ -21,6 +19,7 @@ import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/post/publishers_modal.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
@ -53,7 +52,14 @@ class PostEditScreen extends HookConsumerWidget {
@RoutePage() @RoutePage()
class PostComposeScreen extends HookConsumerWidget { class PostComposeScreen extends HookConsumerWidget {
final SnPost? originalPost; final SnPost? originalPost;
const PostComposeScreen({super.key, this.originalPost}); final SnPost? repliedPost;
final SnPost? forwardedPost;
const PostComposeScreen({
super.key,
this.originalPost,
this.repliedPost,
this.forwardedPost,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -90,9 +96,14 @@ class PostComposeScreen extends HookConsumerWidget {
text: originalPost?.description, text: originalPost?.description,
); );
final contentController = useTextEditingController( final contentController = useTextEditingController(
text: originalPost?.content, text:
originalPost?.content ??
(forwardedPost != null ? '> ${forwardedPost!.content}\n\n' : null),
); );
// Add visibility state with default value from original post or 0 (public)
final visibility = useState<int>(originalPost?.visibility ?? 0);
final submitting = useState(false); final submitting = useState(false);
Future<void> pickPhotoMedia() async { Future<void> pickPhotoMedia() async {
@ -131,7 +142,7 @@ class PostComposeScreen extends HookConsumerWidget {
attachmentProgress.value = {...attachmentProgress.value, index: 0}; attachmentProgress.value = {...attachmentProgress.value, index: 0};
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: attachment.data, fileData: attachment,
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachment.data.name ?? 'Post media', filename: attachment.data.name ?? 'Post media',
@ -188,12 +199,18 @@ class PostComposeScreen extends HookConsumerWidget {
await client.request( await client.request(
originalPost == null ? '/posts' : '/posts/${originalPost!.id}', originalPost == null ? '/posts' : '/posts/${originalPost!.id}',
data: { data: {
'title': titleController.text,
'description': descriptionController.text,
'content': contentController.text, 'content': contentController.text,
'visibility':
visibility.value, // Add visibility field to API request
'attachments': 'attachments':
attachments.value attachments.value
.where((e) => e.isOnCloud) .where((e) => e.isOnCloud)
.map((e) => e.data.id) .map((e) => e.data.id)
.toList(), .toList(),
if (repliedPost != null) 'replied_post_id': repliedPost!.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost!.id,
}, },
options: Options( options: Options(
headers: {'X-Pub': currentPublisher.value?.name}, headers: {'X-Pub': currentPublisher.value?.name},
@ -210,7 +227,7 @@ class PostComposeScreen extends HookConsumerWidget {
} }
} }
Future<void> _handlePaste() async { Future<void> handlePaste() async {
final clipboard = await Pasteboard.image; final clipboard = await Pasteboard.image;
if (clipboard == null) return; if (clipboard == null) return;
@ -223,14 +240,93 @@ class PostComposeScreen extends HookConsumerWidget {
]; ];
} }
void _handleKeyPress(RawKeyEvent event) { void handleKeyPress(RawKeyEvent event) {
if (event is! RawKeyDownEvent) return; if (event is! RawKeyDownEvent) return;
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isModifierPressed = event.isMetaPressed || event.isControlPressed; final isModifierPressed = event.isMetaPressed || event.isControlPressed;
if (isPaste && isModifierPressed) { if (isPaste && isModifierPressed) {
_handlePaste(); handlePaste();
}
}
void showVisibilityModal() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('postVisibility'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Symbols.public),
title: Text('postVisibilityPublic'.tr()),
onTap: () {
visibility.value = 0;
Navigator.pop(context);
},
selected: visibility.value == 0,
),
ListTile(
leading: Icon(Symbols.group),
title: Text('postVisibilityFriends'.tr()),
onTap: () {
visibility.value = 1;
Navigator.pop(context);
},
selected: visibility.value == 1,
),
ListTile(
leading: Icon(Symbols.link_off),
title: Text('postVisibilityUnlisted'.tr()),
onTap: () {
visibility.value = 2;
Navigator.pop(context);
},
selected: visibility.value == 2,
),
ListTile(
leading: Icon(Symbols.lock),
title: Text('postVisibilityPrivate'.tr()),
onTap: () {
visibility.value = 3;
Navigator.pop(context);
},
selected: visibility.value == 3,
),
],
),
),
);
}
// Helper method to get the appropriate icon for each visibility status
IconData getVisibilityIcon(int visibilityValue) {
switch (visibilityValue) {
case 1: // Friends
return Symbols.group;
case 2: // Unlisted
return Symbols.link_off;
case 3: // Private
return Symbols.lock;
default: // Public (0) or unknown
return Symbols.public;
}
}
// Helper method to get the translation key for each visibility status
String getVisibilityText(int visibilityValue) {
switch (visibilityValue) {
case 1: // Friends
return 'postVisibilityFriends';
case 2: // Unlisted
return 'postVisibilityUnlisted';
case 3: // Private
return 'postVisibilityPrivate';
default: // Public (0) or unknown
return 'postVisibilityPublic';
} }
} }
@ -296,6 +392,48 @@ class PostComposeScreen extends HookConsumerWidget {
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (repliedPost != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.5),
child: Row(
children: [
const Icon(Symbols.reply, size: 16),
const Gap(8),
Expanded(
child: Text(
'${'reply'.tr()}: ${repliedPost!.publisher.nick}',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (forwardedPost != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.5),
child: Row(
children: [
const Icon(Symbols.forward, size: 16),
const Gap(8),
Expanded(
child: Text(
'${'forward'.tr()}: ${forwardedPost!.publisher.nick}',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded( Expanded(
child: Row( child: Row(
spacing: 12, spacing: 12,
@ -324,7 +462,52 @@ class PostComposeScreen extends HookConsumerWidget {
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(vertical: 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row(
children: [
OutlinedButton(
onPressed: () {
showVisibilityModal();
},
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
side: BorderSide(
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.5),
),
padding: EdgeInsets.symmetric(horizontal: 16),
visualDensity: const VisualDensity(
vertical: -2,
horizontal: -4,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
getVisibilityIcon(visibility.value),
size: 16,
color:
Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 6),
Text(
getVisibilityText(visibility.value).tr(),
style: TextStyle(
fontSize: 14,
color:
Theme.of(context).colorScheme.primary,
),
),
],
),
),
],
).padding(bottom: 6),
TextField( TextField(
controller: titleController, controller: titleController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
@ -348,7 +531,7 @@ class PostComposeScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
RawKeyboardListener( RawKeyboardListener(
focusNode: FocusNode(), focusNode: FocusNode(),
onKey: _handleKeyPress, onKey: handleKeyPress,
child: TextField( child: TextField(
controller: contentController, controller: contentController,
style: TextStyle(fontSize: 14), style: TextStyle(fontSize: 14),
@ -474,204 +657,3 @@ class PostComposeScreen extends HookConsumerWidget {
); );
} }
} }
class AttachmentPreview extends StatelessWidget {
final UniversalFile item;
final double? progress;
final Function(int)? onMove;
final Function? onDelete;
final Function? onRequestUpload;
const AttachmentPreview({
super.key,
required this.item,
this.progress,
this.onRequestUpload,
this.onMove,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio:
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Builder(
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
if (item.type == UniversalFileType.image) {
return Image.file(File(item.data.path));
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
} else if (item is List<int> || item is Uint8List) {
if (item.type == UniversalFileType.image) {
return Image.memory(item.data);
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
}
return Placeholder();
},
),
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(child: LinearProgressIndicator(value: progress)),
],
),
),
),
Positioned(
left: 8,
top: 8,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
child: Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onDelete != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.delete,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onDelete?.call();
},
),
if (onDelete != null && onMove != null)
SizedBox(
height: 26,
child: const VerticalDivider(
width: 0.3,
color: Colors.white,
thickness: 0.3,
),
).padding(horizontal: 2),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_up,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(-1);
},
),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_down,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(1);
},
),
],
),
),
),
),
),
if (onRequestUpload != null)
Positioned(
top: 8,
right: 8,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => onRequestUpload?.call(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child:
(item.isOnCloud)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-cloud',
style: TextStyle(color: Colors.white),
),
],
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud_off,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-device',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
),
],
),
),
);
}
}

View File

@ -1,4 +1,3 @@
import 'package:auto_route/annotations.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -15,6 +15,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart'; part 'detail.g.dart';
@ -250,6 +251,42 @@ class _RealmActionMenu extends HookConsumerWidget {
} }
} }
@riverpod
class RealmMemberListNotifier extends _$RealmMemberListNotifier
with CursorPagingNotifierMixin<SnRealmMember> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnRealmMember>> build(String realmSlug) async {
return fetch();
}
@override
Future<CursorPagingData<SnRealmMember>> fetch({String? cursor}) async {
final apiClient = ref.read(apiClientProvider);
final offset = cursor != null ? int.parse(cursor) : 0;
final response = await apiClient.get(
'/realms/$realmSlug/members',
queryParameters: {'offset': offset, 'take': _pageSize},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnRealmMember.fromJson(e)).toList();
final hasMore = offset + members.length < total;
final nextCursor = hasMore ? (offset + members.length).toString() : null;
return CursorPagingData(
items: members,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
// Keep the old provider for backward compatibility
final realmMemberStateProvider = final realmMemberStateProvider =
StateNotifierProvider.family<RealmMemberNotifier, RealmMemberState, String>( StateNotifierProvider.family<RealmMemberNotifier, RealmMemberState, String>(
(ref, realmSlug) { (ref, realmSlug) {
@ -302,13 +339,15 @@ class _RealmMemberListSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
final memberListProvider = realmMemberListNotifierProvider(realmSlug);
// For backward compatibility and to show total count in the header
final memberState = ref.watch(realmMemberStateProvider(realmSlug)); final memberState = ref.watch(realmMemberStateProvider(realmSlug));
final memberNotifier = ref.read( final memberNotifier = ref.read(
realmMemberStateProvider(realmSlug).notifier, realmMemberStateProvider(realmSlug).notifier,
); );
final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
useEffect(() { useEffect(() {
Future(() { Future(() {
memberNotifier.loadMore(); memberNotifier.loadMore();
@ -329,8 +368,10 @@ class _RealmMemberListSheet extends HookConsumerWidget {
'/realms/invites/$realmSlug', '/realms/invites/$realmSlug',
data: {'related_user_id': result.id, 'role': 0}, data: {'related_user_id': result.id, 'role': 0},
); );
// Refresh both providers
memberNotifier.reset(); memberNotifier.reset();
await memberNotifier.loadMore(); await memberNotifier.loadMore();
ref.invalidate(memberListProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
} }
@ -362,8 +403,10 @@ class _RealmMemberListSheet extends HookConsumerWidget {
IconButton( IconButton(
icon: const Icon(Symbols.refresh), icon: const Icon(Symbols.refresh),
onPressed: () { onPressed: () {
// Refresh both providers
memberNotifier.reset(); memberNotifier.reset();
memberNotifier.loadMore(); memberNotifier.loadMore();
ref.invalidate(memberListProvider);
}, },
), ),
IconButton( IconButton(
@ -376,108 +419,103 @@ class _RealmMemberListSheet extends HookConsumerWidget {
), ),
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: child: PagingHelperView(
memberState.error != null provider: memberListProvider,
? Center(child: Text(memberState.error!)) futureRefreshable: memberListProvider.future,
: ListView.builder( notifierRefreshable: memberListProvider.notifier,
itemCount: memberState.members.length + 1, contentBuilder: (data, widgetCount, endItemView) {
itemBuilder: (context, index) { return ListView.builder(
if (index == memberState.members.length) { itemCount: widgetCount,
if (memberState.isLoading) { itemBuilder: (context, index) {
return const Center( if (index == data.items.length) {
child: Padding( return endItemView;
padding: EdgeInsets.all(16.0), }
child: CircularProgressIndicator(),
),
);
}
if (memberState.members.length < memberState.total) {
memberNotifier.loadMore(
offset: memberState.members.length,
);
}
return const SizedBox.shrink();
}
final member = memberState.members[index]; final member = data.items[index];
return ListTile( return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12), contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id, fileId: member.account!.profile.picture?.id,
), ),
title: Row( title: Row(
spacing: 6, spacing: 6,
children: [ children: [
Flexible(child: Text(member.account!.nick)), Flexible(child: Text(member.account!.nick)),
if (member.joinedAt == null) if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20), const Icon(Symbols.pending_actions, size: 20),
], ],
), ),
subtitle: Row( subtitle: Row(
children: [ children: [
Text( Text(
member.role >= 100 member.role >= 100
? 'permissionOwner' ? 'permissionOwner'
: member.role >= 50 : member.role >= 50
? 'permissionModerator' ? 'permissionModerator'
: 'permissionMember', : 'permissionMember',
).tr(), ).tr(),
Text('·').bold().padding(horizontal: 6), Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account!.name}")), Expanded(child: Text("@${member.account!.name}")),
], ],
), ),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if ((realmIdentity.value?.role ?? 0) >= 50) if ((realmIdentity.value?.role ?? 0) >= 50)
IconButton( IconButton(
icon: const Icon(Symbols.edit), icon: const Icon(Symbols.edit),
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: builder:
(context) => _RealmMemberRoleSheet( (context) => _RealmMemberRoleSheet(
realmSlug: realmSlug, realmSlug: realmSlug,
member: member, member: member,
), ),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
memberNotifier.reset(); // Refresh both providers
memberNotifier.loadMore(); memberNotifier.reset();
} memberNotifier.loadMore();
}); ref.invalidate(memberListProvider);
}, }
), });
if ((realmIdentity.value?.role ?? 0) >= 50) },
IconButton( ),
icon: const Icon(Symbols.delete), if ((realmIdentity.value?.role ?? 0) >= 50)
onPressed: () { IconButton(
showConfirmAlert( icon: const Icon(Symbols.delete),
'removeRealmMemberHint'.tr(), onPressed: () {
'removeRealmMember'.tr(), showConfirmAlert(
).then((confirm) async { 'removeRealmMemberHint'.tr(),
if (confirm != true) return; 'removeRealmMember'.tr(),
try { ).then((confirm) async {
final apiClient = ref.watch( if (confirm != true) return;
apiClientProvider, try {
); final apiClient = ref.watch(
await apiClient.delete( apiClientProvider,
'/realms/$realmSlug/members/${member.accountId}', );
); await apiClient.delete(
memberNotifier.reset(); '/realms/$realmSlug/members/${member.accountId}',
memberNotifier.loadMore(); );
} catch (err) { // Refresh both providers
showErrorAlert(err); memberNotifier.reset();
} memberNotifier.loadMore();
}); ref.invalidate(memberListProvider);
}, } catch (err) {
), showErrorAlert(err);
], }
), });
); },
}, ),
), ],
),
);
},
);
},
),
), ),
], ],
), ),

View File

@ -148,5 +148,155 @@ class _RealmIdentityProviderElement
String get realmSlug => (origin as RealmIdentityProvider).realmSlug; String get realmSlug => (origin as RealmIdentityProvider).realmSlug;
} }
String _$realmMemberListNotifierHash() =>
r'b2e3eefc62a597f45df9470b2058fdda62f8853f';
abstract class _$RealmMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> {
late final String realmSlug;
FutureOr<CursorPagingData<SnRealmMember>> build(String realmSlug);
}
/// See also [RealmMemberListNotifier].
@ProviderFor(RealmMemberListNotifier)
const realmMemberListNotifierProvider = RealmMemberListNotifierFamily();
/// See also [RealmMemberListNotifier].
class RealmMemberListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnRealmMember>>> {
/// See also [RealmMemberListNotifier].
const RealmMemberListNotifierFamily();
/// See also [RealmMemberListNotifier].
RealmMemberListNotifierProvider call(String realmSlug) {
return RealmMemberListNotifierProvider(realmSlug);
}
@override
RealmMemberListNotifierProvider getProviderOverride(
covariant RealmMemberListNotifierProvider provider,
) {
return call(provider.realmSlug);
}
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'realmMemberListNotifierProvider';
}
/// See also [RealmMemberListNotifier].
class RealmMemberListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
RealmMemberListNotifier,
CursorPagingData<SnRealmMember>
> {
/// See also [RealmMemberListNotifier].
RealmMemberListNotifierProvider(String realmSlug)
: this._internal(
() => RealmMemberListNotifier()..realmSlug = realmSlug,
from: realmMemberListNotifierProvider,
name: r'realmMemberListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmMemberListNotifierHash,
dependencies: RealmMemberListNotifierFamily._dependencies,
allTransitiveDependencies:
RealmMemberListNotifierFamily._allTransitiveDependencies,
realmSlug: realmSlug,
);
RealmMemberListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.realmSlug,
}) : super.internal();
final String realmSlug;
@override
FutureOr<CursorPagingData<SnRealmMember>> runNotifierBuild(
covariant RealmMemberListNotifier notifier,
) {
return notifier.build(realmSlug);
}
@override
Override overrideWith(RealmMemberListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: RealmMemberListNotifierProvider._internal(
() => create()..realmSlug = realmSlug,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
realmSlug: realmSlug,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
RealmMemberListNotifier,
CursorPagingData<SnRealmMember>
>
createElement() {
return _RealmMemberListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmMemberListNotifierProvider &&
other.realmSlug == realmSlug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, realmSlug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmMemberListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnRealmMember>> {
/// The parameter `realmSlug` of this provider.
String get realmSlug;
}
class _RealmMemberListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
RealmMemberListNotifier,
CursorPagingData<SnRealmMember>
>
with RealmMemberListNotifierRef {
_RealmMemberListNotifierProviderElement(super.provider);
@override
String get realmSlug => (origin as RealmMemberListNotifierProvider).realmSlug;
}
// ignore_for_file: type=lint // 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 // 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

@ -16,6 +16,7 @@ import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.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/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -215,7 +216,10 @@ class EditRealmScreen extends HookConsumerWidget {
if (token == null) throw ArgumentError('Access token is null'); if (token == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
@ -394,97 +398,69 @@ class _RealmInviteSheet extends HookConsumerWidget {
} }
} }
return Container( return SheetScaffold(
constraints: BoxConstraints( titleText: 'invites'.tr(),
maxHeight: MediaQuery.of(context).size.height * 0.8, actions: [
), IconButton(
child: Column( icon: const Icon(Symbols.refresh),
mainAxisSize: MainAxisSize.min, style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
children: [ onPressed: () {
Padding( ref.invalidate(realmInvitesProvider);
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), },
child: Row( ),
children: [ ],
Text( child: invites.when(
'invites'.tr(), data:
style: Theme.of(context).textTheme.headlineSmall?.copyWith( (items) =>
fontWeight: FontWeight.w600, items.isEmpty
letterSpacing: -0.5, ? Center(
), child:
), Text(
const Spacer(), 'invitesEmpty',
IconButton( textAlign: TextAlign.center,
icon: const Icon(Symbols.refresh), ).tr(),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)), )
onPressed: () { : ListView.builder(
ref.invalidate(realmInvitesProvider); shrinkWrap: true,
}, itemCount: items.length,
), itemBuilder: (context, index) {
IconButton( final invite = items[index];
icon: const Icon(Symbols.close), return ListTile(
onPressed: () => Navigator.pop(context), leading: ProfilePictureWidget(
style: IconButton.styleFrom(minimumSize: const Size(36, 36)), fileId: invite.realm!.picture?.id,
), fallbackIcon: Symbols.group,
],
),
),
const Divider(height: 1),
Expanded(
child: invites.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
), ),
loading: () => const Center(child: CircularProgressIndicator()), title: Text(invite.realm!.name),
error: subtitle:
(error, _) => ResponseErrorWidget( Text(
error: error, invite.role >= 100
onRetry: () => ref.invalidate(realmInvitesProvider), ? 'permissionOwner'
), : invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(realmInvitesProvider),
), ),
),
],
), ),
); );
} }

View File

@ -147,6 +147,7 @@ class SettingsScreen extends HookConsumerWidget {
title: Text('settingsColorScheme').tr(), title: Text('settingsColorScheme').tr(),
content: SingleChildScrollView( content: SingleChildScrollView(
child: ColorPicker( child: ColorPicker(
paletteType: PaletteType.rgbWithBlue,
enableAlpha: false, enableAlpha: false,
pickerColor: selectedColor, pickerColor: selectedColor,
onColorChanged: (color) { onColorChanged: (color) {
@ -157,7 +158,7 @@ class SettingsScreen extends HookConsumerWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel').tr(), child: Text('cancel').tr(),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -166,7 +167,7 @@ class SettingsScreen extends HookConsumerWidget {
.setAppColorScheme(selectedColor.value); .setAppColorScheme(selectedColor.value);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text('Confirm').tr(), child: Text('confirm').tr(),
), ),
], ],
); );
@ -174,8 +175,9 @@ class SettingsScreen extends HookConsumerWidget {
); );
}, },
child: Container( child: Container(
width: 40, width: 24,
height: 40, height: 24,
margin: EdgeInsets.symmetric(horizontal: 2, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color:
settings.appColorScheme != null settings.appColorScheme != null
@ -198,18 +200,7 @@ class SettingsScreen extends HookConsumerWidget {
title: Text('settingsBackgroundImage').tr(), title: Text('settingsBackgroundImage').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.image), leading: const Icon(Symbols.image),
trailing: Row( trailing: const Icon(Symbols.chevron_right),
mainAxisSize: MainAxisSize.min,
children: [
if (isDesktop)
Tooltip(
message: 'settingsBackgroundImageTooltip'.tr(),
padding: EdgeInsets.only(left: 8),
child: const Icon(Symbols.info, size: 18),
),
const Icon(Symbols.chevron_right),
],
),
onTap: () async { onTap: () async {
final imagePicker = ref.read(imagePickerProvider); final imagePicker = ref.read(imagePickerProvider);
final image = await imagePicker.pickImage( final image = await imagePicker.pickImage(
@ -411,19 +402,19 @@ class SettingsScreen extends HookConsumerWidget {
children: [ children: [
_ShortcutRow( _ShortcutRow(
shortcut: 'Ctrl+F', shortcut: 'Ctrl+F',
description: 'Search', description: 'settingsKeyboardShortcutSearch'.tr(),
), ),
_ShortcutRow( _ShortcutRow(
shortcut: 'Ctrl+,', shortcut: 'Ctrl+,',
description: 'Settings', description: 'settingsKeyboardShortcutSettings'.tr(),
), ),
_ShortcutRow( _ShortcutRow(
shortcut: 'Ctrl+N', shortcut: 'Ctrl+N',
description: 'New Message', description: 'settingsKeyboardShortcutNewMessage'.tr(),
), ),
_ShortcutRow( _ShortcutRow(
shortcut: 'Esc', shortcut: 'Esc',
description: 'Close Dialog', description: 'settingsKeyboardShortcutCloseDialog'.tr(),
), ),
// Add more shortcuts as needed // Add more shortcuts as needed
], ],
@ -432,7 +423,7 @@ class SettingsScreen extends HookConsumerWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(), child: Text('close').tr(),
), ),
], ],
), ),
@ -454,10 +445,10 @@ class SettingsScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_SettingsSection( _SettingsSection(
title: 'Appearance', title: 'settingsAppearance'.tr(),
children: appearanceSettings, children: appearanceSettings,
), ),
_SettingsSection(title: 'Server', children: serverSettings), _SettingsSection(title: 'settingsServer'.tr(), children: serverSettings),
], ],
), ),
), ),
@ -466,12 +457,12 @@ class SettingsScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_SettingsSection( _SettingsSection(
title: 'Behavior', title: 'settingsBehavior'.tr(),
children: behaviorSettings, children: behaviorSettings,
), ),
if (desktopSettings.isNotEmpty) if (desktopSettings.isNotEmpty)
_SettingsSection( _SettingsSection(
title: 'Desktop', title: 'settingsDesktop'.tr(),
children: desktopSettings, children: desktopSettings,
), ),
], ],
@ -484,11 +475,11 @@ class SettingsScreen extends HookConsumerWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_SettingsSection(title: 'Appearance', children: appearanceSettings), _SettingsSection(title: 'settingsAppearance'.tr(), children: appearanceSettings),
_SettingsSection(title: 'Server', children: serverSettings), _SettingsSection(title: 'settingsServer'.tr(), children: serverSettings),
_SettingsSection(title: 'Behavior', children: behaviorSettings), _SettingsSection(title: 'settingsBehavior'.tr(), children: behaviorSettings),
if (desktopSettings.isNotEmpty) if (desktopSettings.isNotEmpty)
_SettingsSection(title: 'Desktop', children: desktopSettings), _SettingsSection(title: 'settingsDesktop'.tr(), children: desktopSettings),
], ],
); );
} }
@ -497,7 +488,7 @@ class SettingsScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
noBackground: false, noBackground: false,
appBar: AppBar( appBar: AppBar(
title: Text('Settings').tr(), title: Text('settings').tr(),
actions: actions:
isDesktop isDesktop
? [ ? [
@ -514,7 +505,7 @@ class SettingsScreen extends HookConsumerWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(), child: Text('close').tr(),
), ),
], ],
), ),

View File

@ -39,7 +39,7 @@ Future<XFile?> cropImage(
} }
Completer<SnCloudFile?> putMediaToCloud({ Completer<SnCloudFile?> putMediaToCloud({
required dynamic fileData, // Can be XFile or List<int> (Uint8List) required UniversalFile fileData,
required String atk, required String atk,
required String baseUrl, required String baseUrl,
String? filename, String? filename,
@ -51,21 +51,27 @@ Completer<SnCloudFile?> putMediaToCloud({
String actualMimetype = mimetype ?? ''; String actualMimetype = mimetype ?? '';
Uint8List? byteData; Uint8List? byteData;
if (fileData is XFile) { // Handle the data based on what's in the UniversalFile
file = fileData; final data = fileData.data;
actualFilename = filename ?? fileData.name;
actualMimetype = mimetype ?? fileData.mimeType ?? ''; if (data is XFile) {
} else if (fileData is List<int> || fileData is Uint8List) { file = data;
byteData = fileData is List<int> ? Uint8List.fromList(fileData) : fileData; actualFilename = filename ?? data.name;
actualMimetype = mimetype ?? data.mimeType ?? '';
} else if (data is List<int> || data is Uint8List) {
byteData = data is List<int> ? Uint8List.fromList(data) : data;
actualFilename = filename ?? 'uploaded_file'; actualFilename = filename ?? 'uploaded_file';
actualMimetype = mimetype ?? 'application/octet-stream'; actualMimetype = mimetype ?? 'application/octet-stream';
if (mimetype == null) { if (mimetype == null) {
throw ArgumentError('Mimetype is required when providing raw bytes.'); throw ArgumentError('Mimetype is required when providing raw bytes.');
} }
file = XFile.fromData(byteData!, mimeType: actualMimetype); 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);
} else { } else {
throw ArgumentError( throw ArgumentError(
'Invalid fileData type. Expected XFile or List<int> (Uint8List).', 'Invalid fileData type. Expected data to be XFile, List<int>, Uint8List, or SnCloudFile.',
); );
} }

View File

@ -0,0 +1,246 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'account_session_sheet.g.dart';
@riverpod
Future<List<SnAuthDevice>> authDevices(Ref ref) async {
final resp = await ref.watch(apiClientProvider).get('/accounts/me/devices');
final sessionId = resp.headers.value('x-auth-session');
final data =
resp.data.map<SnAuthDevice>((e) {
final ele = SnAuthDevice.fromJson(e);
return ele.copyWith(isCurrent: ele.sessions.first.id == sessionId);
}).toList();
return data;
}
class _DeviceListTile extends StatelessWidget {
final SnAuthDevice device;
final Function(String) updateDeviceLabel;
final Function(String) logoutDevice;
const _DeviceListTile({
required this.device,
required this.updateDeviceLabel,
required this.logoutDevice,
});
@override
Widget build(BuildContext context) {
return ListTile(
isThreeLine: true,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Icon(switch (device.platform) {
0 => Icons.device_unknown, // Unidentified
1 => Icons.web, // Web
2 => Icons.phone_iphone, // iOS
3 => Icons.phone_android, // Android
4 => Icons.laptop_mac, // macOS
5 => Icons.window, // Windows
6 => Icons.computer, // Linux
_ => Icons.device_unknown, // fallback
}).padding(top: 4),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('authSessionsCount'.plural(device.sessions.length)),
Text(
'lastActiveAt'.tr(
args: [
DateFormat().format(
device.sessions.first.lastGrantedAt.toLocal(),
),
],
),
),
Text(device.sessions.first.challenge.ipAddress),
if (device.isCurrent)
Row(
children: [
Badge(
backgroundColor: Theme.of(context).colorScheme.primary,
label: Text(
'authDeviceCurrent'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
).padding(top: 4),
],
),
title: Text(device.label ?? device.sessions.first.challenge.userAgent),
trailing:
isWideScreen(context)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit),
tooltip: 'authDeviceEditLabel'.tr(),
onPressed:
() => updateDeviceLabel(device.sessions.first.id),
),
if (!device.isCurrent)
IconButton(
icon: Icon(Icons.logout),
tooltip: 'authDeviceLogout'.tr(),
onPressed: () => logoutDevice(device.sessions.first.id),
),
],
)
: null,
);
}
}
class AccountSessionSheet extends HookConsumerWidget {
const AccountSessionSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authDevices = ref.watch(authDevicesProvider);
void logoutDevice(String sessionId) async {
final confirm = await showConfirmAlert(
'authDeviceLogoutHint'.tr(),
'authDeviceLogout'.tr(),
);
if (!confirm || !context.mounted) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/accounts/me/sessions/$sessionId');
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);
}
}
void updateDeviceLabel(String sessionId) async {
final controller = TextEditingController();
final label = await showDialog<String>(
context: context,
builder:
(context) => AlertDialog(
title: Text('authDeviceLabelTitle'.tr()),
content: TextField(
controller: controller,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
hintText: 'authDeviceLabelHint'.tr(),
),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr()),
),
TextButton(
onPressed: () => Navigator.pop(context, controller.text),
child: Text('confirm'.tr()),
),
],
),
);
if (label == null || label.isEmpty || !context.mounted) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.patch(
'/accounts/me/sessions/$sessionId/label',
data: jsonEncode(label),
);
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);
}
}
final wideScreen = isWideScreen(context);
return SheetScaffold(
titleText: 'authSessions'.tr(),
child: authDevices.when(
data:
(data) => RefreshIndicator(
onRefresh:
() => Future.sync(() => ref.invalidate(authDevicesProvider)),
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.length,
itemBuilder: (context, index) {
final device = data[index];
if (wideScreen) {
return _DeviceListTile(
device: device,
updateDeviceLabel: updateDeviceLabel,
logoutDevice: logoutDevice,
);
} else {
return Dismissible(
key: Key('device-${device.sessions.first.id}'),
direction:
device.isCurrent
? DismissDirection.startToEnd
: DismissDirection.horizontal,
background: Container(
color: Colors.blue,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.edit, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.logout, color: Colors.white),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
updateDeviceLabel(device.sessions.first.id);
return false;
} else {
final confirm = await showConfirmAlert(
'authDeviceLogoutHint'.tr(),
'authDeviceLogout'.tr(),
);
if (confirm && context.mounted) {
logoutDevice(device.sessions.first.id);
}
return false; // Don't dismiss
}
},
child: _DeviceListTile(
device: device,
updateDeviceLabel: updateDeviceLabel,
logoutDevice: logoutDevice,
),
);
}
},
),
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(authDevicesProvider),
),
loading: () => ResponseLoadingWidget(),
),
);
}
}

View File

@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account_session_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$authDevicesHash() => r'19807110962206a9637075d03cd372233cae2f49';
/// See also [authDevices].
@ProviderFor(authDevices)
final authDevicesProvider =
AutoDisposeFutureProvider<List<SnAuthDevice>>.internal(
authDevices,
name: r'authDevicesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$authDevicesHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AuthDevicesRef = AutoDisposeFutureProviderRef<List<SnAuthDevice>>;
// 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

@ -133,54 +133,3 @@ class AccountStatusWidget extends HookConsumerWidget {
).opacity((userStatus.value?.isCustomized ?? false) ? 1 : 0.85); ).opacity((userStatus.value?.isCustomized ?? false) ? 1 : 0.85);
} }
} }
class StatusActivityWidget extends StatelessWidget {
final SnActivity item;
const StatusActivityWidget({super.key, required this.item});
@override
Widget build(BuildContext context) {
final result = SnAccountStatus.fromJson(item.data);
return Row(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: item.account.profile.picture?.id,
radius: 12,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Symbols.circle, size: 12).padding(top: 1, left: 2),
const Gap(4),
Text('status').fontSize(11).tr(),
],
).opacity(0.85),
Text(
result.clearedAt == null
? 'statusActivityTitle'
: 'statusActivityEndedTitle',
)
.tr(
args: [
item.account.nick,
result.label,
RelativeTime(context).format(result.createdAt),
if (result.clearedAt != null)
RelativeTime(context).format(result.clearedAt!),
],
)
.fontSize(13)
.padding(left: 2),
],
),
),
],
).padding(horizontal: 16, vertical: 12);
}
}

View File

@ -290,7 +290,9 @@ class _WebSocketIndicator extends HookConsumerWidget {
return AnimatedPositioned( return AnimatedPositioned(
duration: Duration(milliseconds: 1850), duration: Duration(milliseconds: 1850),
top: top:
!user.hasValue || websocketState == WebSocketState.connected() !user.hasValue ||
user.value == null ||
websocketState == WebSocketState.connected()
? -indicatorHeight ? -indicatorHeight
: 0, : 0,
curve: Curves.fastLinearToSlowEaseIn, curve: Curves.fastLinearToSlowEaseIn,

View File

@ -1,5 +1,9 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart'; import 'package:island/database/message.dart';
@ -27,6 +31,7 @@ class MessageItem extends HookConsumerWidget {
final Function(String action)? onAction; final Function(String action)? onAction;
final Map<int, double>? progress; final Map<int, double>? progress;
final bool showAvatar; final bool showAvatar;
final Function(String messageId) onJump;
const MessageItem({ const MessageItem({
super.key, super.key,
@ -35,6 +40,7 @@ class MessageItem extends HookConsumerWidget {
required this.onAction, required this.onAction,
required this.progress, required this.progress,
required this.showAvatar, required this.showAvatar,
required this.onJump,
}); });
@override @override
@ -54,6 +60,8 @@ class MessageItem extends HookConsumerWidget {
final remoteMessage = message.toRemoteMessage(); final remoteMessage = message.toRemoteMessage();
final sender = remoteMessage.sender; final sender = remoteMessage.sender;
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
return ContextMenuWidget( return ContextMenuWidget(
menuProvider: (_) { menuProvider: (_) {
if (onAction == null) return Menu(children: []); if (onAction == null) return Menu(children: []);
@ -90,6 +98,17 @@ class MessageItem extends HookConsumerWidget {
onAction!.call(MessageItemAction.forward); onAction!.call(MessageItemAction.forward);
}, },
), ),
if (isMobile) MenuSeparator(),
if (isMobile)
MenuAction(
title: 'copyMessage'.tr(),
image: MenuImage.icon(Symbols.copy_all),
callback: () {
Clipboard.setData(
ClipboardData(text: remoteMessage.content ?? ''),
);
},
),
], ],
); );
}, },
@ -355,39 +374,67 @@ class MessageQuoteWidget extends HookConsumerWidget {
if (remoteMessage != null) { if (remoteMessage != null) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
child: Container( child: GestureDetector(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 6), onTap: () {
color: Theme.of( final messageId =
context, isReply
).colorScheme.primaryFixedDim.withOpacity(0.4), ? message.toRemoteMessage().repliedMessageId!
child: Column( : message.toRemoteMessage().forwardedMessageId!;
crossAxisAlignment: CrossAxisAlignment.start, // Find the nearest MessageItem ancestor and call its onJump method
children: [ final MessageItem? ancestor =
if (isReply) context.findAncestorWidgetOfExactType<MessageItem>();
Row( if (ancestor != null) {
mainAxisSize: MainAxisSize.min, ancestor.onJump(messageId);
spacing: 4, }
children: [ },
Icon(Symbols.reply, size: 16, color: textColor), child: Container(
Text( padding: EdgeInsets.symmetric(vertical: 4, horizontal: 6),
'Replying to ${remoteMessage.sender.account.nick}', color: Theme.of(
).textColor(textColor).bold(), context,
], ).colorScheme.primaryFixedDim.withOpacity(0.4),
).padding(right: 8) child: Column(
else crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
mainAxisSize: MainAxisSize.min, if (isReply)
spacing: 4, Row(
children: [ mainAxisSize: MainAxisSize.min,
Icon(Symbols.forward, size: 16, color: textColor), spacing: 4,
Text( children: [
'Forwarded from ${remoteMessage.sender.account.nick}', Icon(Symbols.reply, size: 16, color: textColor),
).textColor(textColor).bold(), Text(
], '${'repliedTo'.tr()} ${remoteMessage.sender.account.nick}',
).padding(right: 8), ).textColor(textColor).bold(),
if (_MessageItemContent.hasContent(remoteMessage)) ],
_MessageItemContent(item: remoteMessage), ).padding(right: 8)
], else
Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Icon(Symbols.forward, size: 16, color: textColor),
Text(
'${'forwarded'.tr()} ${remoteMessage.sender.account.nick}',
).textColor(textColor).bold(),
],
).padding(right: 8),
if (_MessageItemContent.hasContent(remoteMessage))
_MessageItemContent(item: remoteMessage),
if (remoteMessage.attachments.isNotEmpty)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Symbols.attach_file, size: 12, color: textColor),
const SizedBox(width: 4),
Text(
'hasAttachments'.plural(
remoteMessage.attachments.length,
),
style: TextStyle(color: textColor, fontSize: 12),
),
],
).padding(vertical: 2),
],
),
), ),
), ),
).padding(bottom: 4); ).padding(bottom: 4);
@ -414,7 +461,7 @@ class _MessageItemContent extends StatelessWidget {
); );
case 'text': case 'text':
default: default:
return MarkdownTextContent(content: item.content!); return MarkdownTextContent(content: item.content!, isSelectable: true);
} }
} }

View File

@ -20,6 +20,9 @@ String _parseRemoteError(DioException err) {
} }
void showErrorAlert(dynamic err) async { void showErrorAlert(dynamic err) async {
if (err is Error) {
log('${err.stackTrace}');
}
final text = switch (err) { final text = switch (err) {
String _ => err, String _ => err,
DioException _ => _parseRemoteError(err), DioException _ => _parseRemoteError(err),

View File

@ -0,0 +1,228 @@
import 'dart:io';
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/file.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class AttachmentPreview extends StatelessWidget {
final UniversalFile item;
final double? progress;
final Function(int)? onMove;
final Function? onDelete;
final Function? onRequestUpload;
const AttachmentPreview({
super.key,
required this.item,
this.progress,
this.onRequestUpload,
this.onMove,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio:
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Builder(
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
if (item.type == UniversalFileType.image) {
final file = item.data as XFile;
if (file.path.isEmpty) {
return FutureBuilder<Uint8List>(
future: file.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
return kIsWeb
? Image.network(file.path)
: Image.file(File(file.path));
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
} else if (item is List<int> || item is Uint8List) {
if (item.type == UniversalFileType.image) {
return Image.memory(item.data);
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
}
return Placeholder();
},
),
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(child: LinearProgressIndicator(value: progress)),
],
),
),
),
Positioned(
left: 8,
top: 8,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
child: Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onDelete != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.delete,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onDelete?.call();
},
),
if (onDelete != null && onMove != null)
SizedBox(
height: 26,
child: const VerticalDivider(
width: 0.3,
color: Colors.white,
thickness: 0.3,
),
).padding(horizontal: 2),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_up,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(-1);
},
),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_down,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(1);
},
),
],
),
),
),
),
),
if (onRequestUpload != null)
Positioned(
top: 8,
right: 8,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => onRequestUpload?.call(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child:
(item.isOnCloud)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-cloud',
style: TextStyle(color: Colors.white),
),
],
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud_off,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-device',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
),
],
),
),
);
}
}

View File

@ -6,13 +6,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:gal/gal.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/widgets/content/cloud_files.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:photo_view/photo_view.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:dio/dio.dart';
class CloudFileList extends HookConsumerWidget { class CloudFileList extends HookConsumerWidget {
final List<SnCloudFile> files; final List<SnCloudFile> files;
@ -110,6 +114,7 @@ class CloudFileList extends HookConsumerWidget {
heroTag: heroTags[i], heroTag: heroTags[i],
isImage: files[i].mimeType?.startsWith('image') ?? false, isImage: files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn, disableZoomIn: disableZoomIn,
fit: BoxFit.cover,
), ),
], ],
onTap: (i) { onTap: (i) {
@ -175,6 +180,47 @@ class CloudFileZoomIn extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final photoViewController = useMemoized(() => PhotoViewController(), []); final photoViewController = useMemoized(() => PhotoViewController(), []);
final rotation = useState(0);
Future<void> saveToGallery() async {
try {
// Show loading indicator
final scaffold = ScaffoldMessenger.of(context);
scaffold.showSnackBar(
const SnackBar(
content: Text('Saving image to gallery...'),
duration: Duration(seconds: 1),
),
);
// Get the image URL
final imageUrl = '$serverUrl/files/${item.id}?original=true';
// 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);
// Show success message
scaffold.showSnackBar(
const SnackBar(
content: Text('Image saved to gallery'),
duration: Duration(seconds: 2),
),
);
} catch (e) {
// Show error message
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to save image: $e'),
duration: const Duration(seconds: 2),
),
);
}
}
return DismissiblePage( return DismissiblePage(
isFullScreen: true, isFullScreen: true,
@ -195,16 +241,119 @@ class CloudFileZoomIn extends HookConsumerWidget {
imageProvider: CloudImageWidget.provider( imageProvider: CloudImageWidget.provider(
fileId: item.id, fileId: item.id,
serverUrl: serverUrl, serverUrl: serverUrl,
original: true,
), ),
// Apply rotation transformation
customSize: MediaQuery.of(context).size,
basePosition: Alignment.center,
filterQuality: FilterQuality.high,
), ),
), ),
// Close button // Close button and save button
Positioned( Positioned(
top: MediaQuery.of(context).padding.top + 20, top: MediaQuery.of(context).padding.top + 16,
right: 20, right: 16,
child: IconButton( left: 16,
icon: Icon(Icons.close, color: Colors.white), child: Row(
onPressed: () => Navigator.of(context).pop(), mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
icon: Icon(
Icons.save_alt,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () async {
saveToGallery();
},
),
],
),
IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Rotation controls
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 16,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(Icons.remove, color: Colors.white),
onPressed: () {
photoViewController.scale =
(photoViewController.scale ?? 1) - 0.05;
},
),
IconButton(
icon: Icon(Icons.add, color: Colors.white),
onPressed: () {
photoViewController.scale =
(photoViewController.scale ?? 1) + 0.05;
},
),
const Gap(8),
IconButton(
icon: Icon(
Icons.rotate_left,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () {
rotation.value = (rotation.value - 1) % 4;
photoViewController.rotation =
rotation.value * -math.pi / 2;
},
),
IconButton(
icon: Icon(
Icons.rotate_right,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () {
rotation.value = (rotation.value + 1) % 4;
photoViewController.rotation =
rotation.value * -math.pi / 2;
},
),
],
), ),
), ),
], ],
@ -219,6 +368,7 @@ class _CloudFileListEntry extends StatelessWidget {
final bool isImage; final bool isImage;
final bool disableZoomIn; final bool disableZoomIn;
final VoidCallback? onTap; final VoidCallback? onTap;
final BoxFit fit;
const _CloudFileListEntry({ const _CloudFileListEntry({
required this.file, required this.file,
@ -226,11 +376,13 @@ class _CloudFileListEntry extends StatelessWidget {
required this.isImage, required this.isImage,
required this.disableZoomIn, required this.disableZoomIn,
this.onTap, this.onTap,
this.fit = BoxFit.contain,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final content = Stack( final content = Stack(
fit: StackFit.expand,
children: [ children: [
if (isImage) if (isImage)
Positioned.fill( Positioned.fill(
@ -247,9 +399,10 @@ class _CloudFileListEntry extends StatelessWidget {
item: file, item: file,
heroTag: heroTag, heroTag: heroTag,
noBlurhash: true, noBlurhash: true,
).center() fit: fit,
)
else else
CloudFileWidget(item: file, heroTag: heroTag), CloudFileWidget(item: file, heroTag: heroTag, fit: fit),
], ],
); );

View File

@ -8,9 +8,9 @@ import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -56,7 +56,7 @@ class CloudFilePicker extends HookConsumerWidget {
final file = files.value[idx]; final file = files.value[idx];
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: file.data, fileData: file,
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: file.data.name ?? 'Post media', filename: file.data.name ?? 'Post media',

View File

@ -79,8 +79,9 @@ class CloudImageWidget extends ConsumerWidget {
static ImageProvider provider({ static ImageProvider provider({
required String fileId, required String fileId,
required String serverUrl, required String serverUrl,
bool original = false,
}) { }) {
final uri = '$serverUrl/files/$fileId'; final uri = '$serverUrl/files/$fileId?original=$original';
return CachedNetworkImageProvider(uri); return CachedNetworkImageProvider(uri);
} }
} }
@ -118,7 +119,7 @@ class ProfilePictureWidget extends ConsumerWidget {
fallbackColor ?? fallbackColor ??
Theme.of(context).colorScheme.onPrimaryContainer, Theme.of(context).colorScheme.onPrimaryContainer,
).center() ).center()
: CachedNetworkImage(imageUrl: uri, fit: BoxFit.cover), : UniversalImage(uri: uri, fit: BoxFit.cover),
), ),
); );
} }
@ -297,7 +298,7 @@ class SplitAvatarWidget extends ConsumerWidget {
return SizedBox( return SizedBox(
width: radius, width: radius,
height: radius, height: radius,
child: CachedNetworkImage(imageUrl: uri, fit: BoxFit.cover), child: UniversalImage(uri: uri, fit: BoxFit.cover),
); );
} }
} }

View File

@ -45,6 +45,16 @@ class UniversalImage extends StatelessWidget {
height: height, height: height,
memCacheHeight: cacheHeight, memCacheHeight: cacheHeight,
memCacheWidth: cacheWidth, memCacheWidth: cacheWidth,
progressIndicatorBuilder: (context, url, progress) {
return Center(
child: CircularProgressIndicator(value: progress.progress),
);
},
errorWidget: (context, url, error) {
return const Center(
child: Icon(Icons.broken_image, color: Colors.white, size: 16),
);
},
), ),
], ],
), ),

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
class SheetScaffold extends StatelessWidget {
final Widget? title;
final String? titleText;
final List<Widget> actions;
final Widget child;
final double heightFactor;
const SheetScaffold({
super.key,
this.title,
this.titleText,
required this.child,
this.actions = const [],
this.heightFactor = 0.8,
});
@override
Widget build(BuildContext context) {
assert(title != null || titleText != null);
var titleWidget =
title ??
Text(
titleText!,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
);
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * heightFactor,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
titleWidget,
const Spacer(),
...actions,
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(child: child),
],
),
);
}
}

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video.dart';
@ -37,26 +36,16 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url); final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) { if (inCacheInfo == null) {
log('[MediaPlayer] Miss cache: $url'); log('[MediaPlayer] Miss cache: $url');
final token = await getToken(ref.watch(tokenProvider)); final token = ref.watch(tokenProvider)?.token;
final fileStream = DefaultCacheManager().getFileStream( DefaultCacheManager().downloadFile(
url, url,
headers: {'Authorization': 'Bearer $token'}, authHeaders: {'Authorization': 'AtField $token'},
withProgress: true,
); );
await for (var fileInfo in fileStream) { uri = url;
if (fileInfo is FileInfo) {
uri = fileInfo.file.path;
break;
}
}
} else { } else {
uri = inCacheInfo.file.path; uri = inCacheInfo.file.path;
log('[MediaPlayer] Hit cache: $url'); log('[MediaPlayer] Hit cache: $url');
} }
if (uri == null) {
showErrorAlert('Failed to open media... $url');
return;
}
_player!.open(Media(uri), play: false); _player!.open(Media(uri), play: false);
} }

View File

@ -25,6 +25,7 @@ class PostItem extends HookConsumerWidget {
final SnPost item; final SnPost item;
final EdgeInsets? padding; final EdgeInsets? padding;
final bool isOpenable; final bool isOpenable;
final bool showReferencePost;
final Function? onRefresh; final Function? onRefresh;
final Function(SnPost)? onUpdate; final Function(SnPost)? onUpdate;
const PostItem({ const PostItem({
@ -33,6 +34,7 @@ class PostItem extends HookConsumerWidget {
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
this.isOpenable = true, this.isOpenable = true,
this.showReferencePost = true,
this.onRefresh, this.onRefresh,
this.onUpdate, this.onUpdate,
}); });
@ -101,6 +103,20 @@ class PostItem extends HookConsumerWidget {
); );
}, },
), ),
MenuAction(
title: 'reply'.tr(),
image: MenuImage.icon(Symbols.reply),
callback: () {
context.router.push(PostComposeRoute(repliedPost: item));
},
),
MenuAction(
title: 'forward'.tr(),
image: MenuImage.icon(Symbols.forward),
callback: () {
context.router.push(PostComposeRoute(forwardedPost: item));
},
),
], ],
); );
}, },
@ -132,8 +148,52 @@ class PostItem extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(item.publisher.nick).bold(), Text(item.publisher.nick).bold(),
// Add visibility indicator if not public (visibility != 0)
if (item.visibility != 0)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getVisibilityIcon(item.visibility),
size: 14,
color:
Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
_getVisibilityText(item.visibility).tr(),
style: TextStyle(
fontSize: 12,
color:
Theme.of(context).colorScheme.secondary,
),
),
],
).padding(top: 2, bottom: 2),
if (item.title?.isNotEmpty ?? false)
Text(
item.title!,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
if (item.description?.isNotEmpty ?? false)
Text(
item.description!,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
).padding(bottom: 8),
if (item.content?.isNotEmpty ?? false) if (item.content?.isNotEmpty ?? false)
MarkdownTextContent(content: item.content!), MarkdownTextContent(content: item.content!),
if ((item.repliedPost != null ||
item.forwardedPost != null) &&
showReferencePost)
_buildReferencePost(context, item),
if (item.attachments.isNotEmpty) if (item.attachments.isNotEmpty)
CloudFileList( CloudFileList(
files: item.attachments, files: item.attachments,
@ -178,6 +238,141 @@ class PostItem extends HookConsumerWidget {
} }
} }
Widget _buildReferencePost(BuildContext context, SnPost item) {
final referencePost = item.repliedPost ?? item.forwardedPost;
if (referencePost == null) return const SizedBox.shrink();
final isReply = item.repliedPost != null;
return Container(
margin: const EdgeInsets.only(top: 8, bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isReply ? Symbols.reply : Symbols.forward,
size: 16,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 6),
Text(
isReply ? 'repliedTo'.tr() : 'forwarded'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
],
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: referencePost.publisher.picture?.id,
radius: 16,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
referencePost.publisher.nick,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
// Add visibility indicator for referenced post if not public
if (referencePost.visibility != 0)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getVisibilityIcon(referencePost.visibility),
size: 12,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
_getVisibilityText(referencePost.visibility).tr(),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.secondary,
),
),
],
).padding(top: 2, bottom: 2),
if (referencePost.title?.isNotEmpty ?? false)
Text(
referencePost.title!,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: Theme.of(context).colorScheme.onSurface,
),
).padding(top: 2, bottom: 2),
if (referencePost.description?.isNotEmpty ?? false)
Text(
referencePost.description!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
).padding(bottom: 2),
if (referencePost.content?.isNotEmpty ?? false)
MarkdownTextContent(
content: referencePost.content!,
textStyle: const TextStyle(fontSize: 14),
isSelectable: false,
).padding(bottom: 4),
if (referencePost.attachments.isNotEmpty)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.attach_file,
size: 12,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
'postHasAttachments'.plural(
referencePost.attachments.length,
),
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontSize: 12,
),
),
],
).padding(vertical: 2),
],
),
),
],
),
],
),
).gestures(
onTap: () => context.router.push(PostDetailRoute(id: referencePost.id)),
);
}
class PostReactionList extends HookConsumerWidget { class PostReactionList extends HookConsumerWidget {
final String parentId; final String parentId;
final Map<String, int> reactions; final Map<String, int> reactions;
@ -388,3 +583,31 @@ class _PostReactionSheet extends StatelessWidget {
); );
} }
} }
// Helper method to get the appropriate icon for each visibility status
IconData _getVisibilityIcon(int visibility) {
switch (visibility) {
case 1: // Friends
return Symbols.group;
case 2: // Unlisted
return Symbols.link_off;
case 3: // Private
return Symbols.lock;
default: // Public (0) or unknown
return Symbols.public;
}
}
// Helper method to get the translation key for each visibility status
String _getVisibilityText(int visibility) {
switch (visibility) {
case 1: // Friends
return 'postVisibilityFriends';
case 2: // Unlisted
return 'postVisibilityUnlisted';
case 3: // Private
return 'postVisibilityPrivate';
default: // Public (0) or unknown
return 'postVisibilityPublic';
}
}

View File

@ -14,11 +14,8 @@ class PostListNotifier extends _$PostListNotifier
with CursorPagingNotifierMixin<SnPost> { with CursorPagingNotifierMixin<SnPost> {
static const int _pageSize = 20; static const int _pageSize = 20;
String? pubName;
@override @override
Future<CursorPagingData<SnPost>> build(String? pubName) { Future<CursorPagingData<SnPost>> build(String? pubName) {
this.pubName = pubName;
return fetch(cursor: null); return fetch(cursor: null);
} }

View File

@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$postListNotifierHash() => r'6568b7a5afad71551009d9bc7af26afb4b07c9e5'; String _$postListNotifierHash() => r'a2a273cbf96393a84a66bd6ae8e88058704f3195';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@ -94,6 +94,7 @@ class PostRepliesList extends HookConsumerWidget {
PostItem( PostItem(
item: data.items[index], item: data.items[index],
backgroundColor: isWide ? Colors.transparent : null, backgroundColor: isWide ? Colors.transparent : null,
showReferencePost: false,
), ),
const Divider(height: 1), const Divider(height: 1),
], ],

View File

@ -9,7 +9,11 @@ class ResponseErrorWidget extends StatelessWidget {
final dynamic error; final dynamic error;
final VoidCallback onRetry; final VoidCallback onRetry;
const ResponseErrorWidget({super.key, required this.error, required this.onRetry}); const ResponseErrorWidget({
super.key,
required this.error,
required this.onRetry,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -36,6 +40,15 @@ class ResponseErrorWidget extends StatelessWidget {
], ],
), ),
).center() ).center()
else if (error is DioException && error.response != null)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Text(
error.response.toString(),
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
).center()
else else
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320), constraints: const BoxConstraints(maxWidth: 320),

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
class TechicalReviewIntroWidget extends StatelessWidget { class TechicalReviewIntroWidget extends StatelessWidget {
@ -8,55 +8,26 @@ class TechicalReviewIntroWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return SheetScaffold(
constraints: BoxConstraints( titleText: '技术性预览',
maxHeight: MediaQuery.of(context).size.height * 0.8, child: SingleChildScrollView(
), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Padding( Text('👋').fontSize(32),
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), Text('你好呀~').fontSize(24),
child: Row( Text('欢迎来使用 Solar Network 3.0 的技术性预览版。'),
children: [ const Gap(24),
Text( Text('技术性预览的初衷是让我们更顺滑的将 3.0 发布出来,帮助我们一点一点的迁移数据。'),
'技术性预览', const Gap(24),
style: Theme.of(context).textTheme.headlineSmall?.copyWith( Text('同时,既然是测试版,肯定有一系列的 Bug 和问题,请多多包涵,也欢迎积极反馈到 GitHub 上。'),
fontWeight: FontWeight.w600, Text('目前帐号数据已经迁移完毕,其他数据将在未来逐渐迁移。还请耐心等待,不要重复创建以免未来数据冲突。'),
letterSpacing: -0.5, const Gap(24),
), Text('最后,感谢你愿意参与技术性预览,祝你使用愉快!'),
), const Gap(16),
const Spacer(), Text('关掉这个对话框就开始探索吧!').fontSize(11),
IconButton( ],
icon: const Icon(Symbols.close), ).padding(horizontal: 20, vertical: 24),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('👋').fontSize(32),
Text('你好呀~').fontSize(24),
Text('欢迎来使用 Solar Network 3.0 的技术性预览版。'),
const Gap(24),
Text('技术性预览的初衷是让我们更顺滑的将 3.0 发布出来,帮助我们一点一点的迁移数据。'),
const Gap(24),
Text('同时,既然是测试版,肯定有一系列的 Bug 和问题,请多多包涵,也欢迎积极反馈到 GitHub 上。'),
Text('目前帐号数据已经迁移完毕,其他数据将在未来逐渐迁移。还请耐心等待,不要重复创建以免未来数据冲突。'),
const Gap(24),
Text('最后,感谢你愿意参与技术性预览,祝你使用愉快!'),
const Gap(16),
Text('关掉这个对话框就开始探索吧!').fontSize(11),
],
).padding(horizontal: 20, vertical: 24),
),
),
],
), ),
); );
} }

View File

@ -15,6 +15,7 @@
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h> #include <pasteboard/pasteboard_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h> #include <super_native_extensions/super_native_extensions_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
@ -48,6 +49,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) pasteboard_registrar = g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar); pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

View File

@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video
pasteboard pasteboard
record_linux
sqlite3_flutter_libs sqlite3_flutter_libs
super_native_extensions super_native_extensions
url_launcher_linux url_launcher_linux

View File

@ -23,6 +23,7 @@ import media_kit_video
import package_info_plus import package_info_plus
import pasteboard import pasteboard
import path_provider_foundation import path_provider_foundation
import record_macos
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import sqlite3_flutter_libs import sqlite3_flutter_libs
@ -50,6 +51,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))

View File

@ -107,6 +107,8 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- record_macos (1.0.0):
- FlutterMacOS
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
@ -167,6 +169,7 @@ DEPENDENCIES:
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
@ -232,6 +235,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos :path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
record_macos:
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
shared_preferences_foundation: shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin: sqflite_darwin:
@ -278,6 +283,7 @@ SPEC CHECKSUMS:
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0

View File

@ -15,7 +15,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>Solian</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -827,6 +827,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.6" version: "2.4.6"
flutter_otp_text_field:
dependency: "direct main"
description:
name: flutter_otp_text_field
sha256: e7e589dc51cde120d63da6db55f3cef618f5d013d12adba76137ca1a51ce1390
url: "https://pub.dev"
source: hosted
version: "1.5.1+1"
flutter_platform_alert: flutter_platform_alert:
dependency: "direct main" dependency: "direct main"
description: description:
@ -917,6 +925,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
gal:
dependency: "direct main"
description:
name: gal
sha256: "1bdef5879e4569910cfd8c77f460f98fcb7a1f910026af1daa80869856c67d66"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
gap: gap:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1542,6 +1558,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
recase: recase:
dependency: transitive dependency: transitive
description: description:
@ -1550,6 +1582,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
record:
dependency: "direct main"
description:
name: record
sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817
url: "https://pub.dev"
source: hosted
version: "6.0.0"
record_android:
dependency: transitive
description:
name: record_android
sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
record_ios:
dependency: transitive
description:
name: record_ios
sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: "29e7735b05c1944bb6c9b72a36c08d4a1b24117e712d6a9523c003bde12bf484"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
record_macos:
dependency: transitive
description:
name: record_macos
sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: f8e536a9c927e52f95326d7540898457eaeefbe0b21a84d3cb3d2d7d4645e8cb
url: "https://pub.dev"
source: hosted
version: "1.1.7"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
relative_time: relative_time:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1977,7 +2073,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "55fd380bcca8c984773711062ac7dfdbfa87c9d1" resolved-ref: "55e0eecfb7a7af67be4a7b6e8e73d128d4460436"
url: "https://github.com/LittleSheep2Code/tus_client.git" url: "https://github.com/LittleSheep2Code/tus_client.git"
source: git source: git
version: "2.5.0" version: "2.5.0"

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 3.0.0+100 version: 3.0.0+104
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2
@ -98,12 +98,16 @@ dependencies:
visibility_detector: ^0.4.0+2 visibility_detector: ^0.4.0+2
flutter_native_splash: ^2.4.6 flutter_native_splash: ^2.4.6
photo_view: ^0.15.0 photo_view: ^0.15.0
gal: ^1.9.1
dismissible_page: ^1.0.2 dismissible_page: ^1.0.2
super_sliver_list: ^0.4.1 super_sliver_list: ^0.4.1
flutter_webrtc: ^0.14.1 flutter_webrtc: ^0.14.1
livekit_client: ^2.4.7 livekit_client: ^2.4.7
pasteboard: ^0.4.0 pasteboard: ^0.4.0
flutter_colorpicker: ^1.1.0 flutter_colorpicker: ^1.1.0
record: ^6.0.0
qr_flutter: ^4.1.0
flutter_otp_text_field: ^1.5.1+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -19,6 +19,7 @@
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h> #include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h> #include <pasteboard/pasteboard_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin_c_api.h> #include <super_native_extensions/super_native_extensions_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@ -51,6 +52,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
PasteboardPluginRegisterWithRegistrar( PasteboardPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PasteboardPlugin")); registry->GetRegistrarForPlugin("PasteboardPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar( Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
SuperNativeExtensionsPluginCApiRegisterWithRegistrar( SuperNativeExtensionsPluginCApiRegisterWithRegistrar(

View File

@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_libs_windows_video media_kit_libs_windows_video
media_kit_video media_kit_video
pasteboard pasteboard
record_windows
sqlite3_flutter_libs sqlite3_flutter_libs
super_native_extensions super_native_extensions
url_launcher_windows url_launcher_windows