mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-13 13:02:13 +00:00
Merge pull request #418 from twonlyapp/dev
- Fix: Changed minimum threshold for the user discovery to 3 - Fix: Multiple UI issues
This commit is contained in:
commit
b192945328
92 changed files with 3711 additions and 2031 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,15 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.30
|
||||||
|
|
||||||
|
- Fix: Changed minimum threshold for the user discovery to 3
|
||||||
|
|
||||||
|
## 0.2.28
|
||||||
|
|
||||||
|
- Improved: Design of some UI components
|
||||||
|
- Improved: Memories viewer shows state for batch operations and has improved performance
|
||||||
|
- Fix: Auto-detect if FCM token does not work and trigger a reset
|
||||||
|
|
||||||
## 0.2.26
|
## 0.2.26
|
||||||
|
|
||||||
- New: Import images from the gallery
|
- New: Import images from the gallery
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ analyzer:
|
||||||
- "lib/core/**"
|
- "lib/core/**"
|
||||||
- "lib/src/localization/**"
|
- "lib/src/localization/**"
|
||||||
- "rust_builder/"
|
- "rust_builder/"
|
||||||
|
- "build/"
|
||||||
- "dependencies/**"
|
- "dependencies/**"
|
||||||
- "pubspec.yaml"
|
- "pubspec.yaml"
|
||||||
- "**.arb"
|
- "**.arb"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ plugins {
|
||||||
// START: FlutterFire Configuration
|
// START: FlutterFire Configuration
|
||||||
id 'com.google.gms.google-services'
|
id 'com.google.gms.google-services'
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
id "kotlin-android"
|
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip
|
||||||
|
|
@ -18,11 +18,11 @@ pluginManagement {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.9.1' apply false
|
id "com.android.application" version '8.11.1' apply false
|
||||||
// START: FlutterFire Configuration
|
// START: FlutterFire Configuration
|
||||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||||
|
|
||||||
|
// Store the current timestamp in Keychain for iOS FCM messaging tracking
|
||||||
|
let nowMs = String(format: "%.0f", Date().timeIntervalSince1970 * 1000)
|
||||||
|
writeToKeychain(key: "last_fcm_message_timestamp", value: nowMs)
|
||||||
|
NSLog("Received APNs push notification, updated last_fcm_message_timestamp to \(nowMs)")
|
||||||
|
|
||||||
if let bestAttemptContent = bestAttemptContent {
|
if let bestAttemptContent = bestAttemptContent {
|
||||||
|
|
||||||
guard bestAttemptContent.userInfo as? [String: Any] != nil,
|
guard bestAttemptContent.userInfo as? [String: Any] != nil,
|
||||||
|
|
@ -188,6 +193,7 @@ func readFromKeychain(key: String) -> String? {
|
||||||
let query: [String: Any] = [
|
let query: [String: Any] = [
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
kSecAttrAccount as String: key,
|
kSecAttrAccount as String: key,
|
||||||
|
kSecAttrService as String: "flutter_secure_storage_service",
|
||||||
kSecReturnData as String: kCFBooleanTrue!,
|
kSecReturnData as String: kCFBooleanTrue!,
|
||||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||||
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared", // Use your access group
|
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared", // Use your access group
|
||||||
|
|
@ -205,6 +211,36 @@ func readFromKeychain(key: String) -> String? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to write to Keychain
|
||||||
|
func writeToKeychain(key: String, value: String) {
|
||||||
|
guard let data = value.data(using: .utf8) else {
|
||||||
|
NSLog("Failed to convert value to data for keychain key: \(key)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecAttrService as String: "flutter_secure_storage_service",
|
||||||
|
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared"
|
||||||
|
]
|
||||||
|
|
||||||
|
// Delete existing item first to ensure a clean overwrite
|
||||||
|
SecItemDelete(query as CFDictionary)
|
||||||
|
|
||||||
|
// Add the new item with background-compatible accessibility
|
||||||
|
var addQuery = query
|
||||||
|
addQuery[kSecValueData as String] = data
|
||||||
|
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||||
|
|
||||||
|
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
NSLog("Failed to write keychain item for key \(key): \(status)")
|
||||||
|
} else {
|
||||||
|
NSLog("Successfully wrote keychain item for key: \(key)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) {
|
func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) {
|
||||||
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
||||||
|
|
||||||
|
|
|
||||||
33
ios/Podfile
33
ios/Podfile
|
|
@ -28,17 +28,9 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
|
||||||
|
|
||||||
flutter_ios_podfile_setup
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
pod 'Firebase', :modular_headers => true
|
|
||||||
pod 'FirebaseMessaging', :modular_headers => true
|
|
||||||
pod 'FirebaseCoreInternal', :modular_headers => true
|
|
||||||
pod 'GoogleUtilities', :modular_headers => true
|
|
||||||
pod 'FirebaseCore', :modular_headers => true
|
|
||||||
pod 'SwiftProtobuf'
|
|
||||||
# pod 'sqlite3', :modular_headers => true
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
target 'Runner' do
|
target 'Runner' do
|
||||||
|
pod 'SwiftProtobuf'
|
||||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
target 'RunnerTests' do
|
target 'RunnerTests' do
|
||||||
inherit! :search_paths
|
inherit! :search_paths
|
||||||
|
|
@ -76,6 +68,28 @@ post_install do |installer|
|
||||||
|
|
||||||
## dart: PermissionGroup.mediaLibrary
|
## dart: PermissionGroup.mediaLibrary
|
||||||
'PERMISSION_PHOTOS=1',
|
'PERMISSION_PHOTOS=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse
|
||||||
|
'PERMISSION_LOCATION=0',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.contacts
|
||||||
|
'PERMISSION_CONTACTS=0',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.calendar, PermissionGroup.reminders
|
||||||
|
'PERMISSION_EVENTS=0',
|
||||||
|
'PERMISSION_REMINDERS=0',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.speech
|
||||||
|
'PERMISSION_SPEECH_RECOGNITION=0',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.bluetooth
|
||||||
|
'PERMISSION_BLUETOOTH=0',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.appTrackingTransparency
|
||||||
|
'PERMISSION_APP_TRACKING_TRANSPARENCY=0',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.sensors
|
||||||
|
'PERMISSION_SENSORS=0',
|
||||||
]
|
]
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
@ -83,5 +97,6 @@ post_install do |installer|
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'NotificationService' do
|
target 'NotificationService' do
|
||||||
|
pod 'SwiftProtobuf'
|
||||||
# pod 'Firebase/Messaging'
|
# pod 'Firebase/Messaging'
|
||||||
end
|
end
|
||||||
|
|
|
||||||
355
ios/Podfile.lock
355
ios/Podfile.lock
|
|
@ -1,136 +1,18 @@
|
||||||
PODS:
|
PODS:
|
||||||
- app_links (7.0.0):
|
|
||||||
- Flutter
|
|
||||||
- audio_waveforms (0.0.1):
|
- audio_waveforms (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- background_downloader (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- camera_avfoundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- connectivity_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- cryptography_flutter_plus (0.2.0):
|
- cryptography_flutter_plus (0.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- device_info_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- DKImagePickerController/Core (4.3.9):
|
|
||||||
- DKImagePickerController/ImageDataManager
|
|
||||||
- DKImagePickerController/Resource
|
|
||||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
|
||||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
|
||||||
- DKImagePickerController/Core
|
|
||||||
- DKPhotoGallery
|
|
||||||
- DKImagePickerController/Resource (4.3.9)
|
|
||||||
- DKPhotoGallery (0.0.19):
|
|
||||||
- DKPhotoGallery/Core (= 0.0.19)
|
|
||||||
- DKPhotoGallery/Model (= 0.0.19)
|
|
||||||
- DKPhotoGallery/Preview (= 0.0.19)
|
|
||||||
- DKPhotoGallery/Resource (= 0.0.19)
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Core (0.0.19):
|
|
||||||
- DKPhotoGallery/Model
|
|
||||||
- DKPhotoGallery/Preview
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Model (0.0.19):
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Preview (0.0.19):
|
|
||||||
- DKPhotoGallery/Model
|
|
||||||
- DKPhotoGallery/Resource
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Resource (0.0.19):
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- emoji_picker_flutter (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- file_picker (0.0.1):
|
|
||||||
- DKImagePickerController/PhotoGallery
|
|
||||||
- Flutter
|
|
||||||
- Firebase (12.9.0):
|
|
||||||
- Firebase/Core (= 12.9.0)
|
|
||||||
- Firebase/Core (12.9.0):
|
|
||||||
- Firebase/CoreOnly
|
|
||||||
- FirebaseAnalytics (~> 12.9.0)
|
|
||||||
- Firebase/CoreOnly (12.9.0):
|
|
||||||
- FirebaseCore (~> 12.9.0)
|
|
||||||
- Firebase/Installations (12.9.0):
|
|
||||||
- Firebase/CoreOnly
|
|
||||||
- FirebaseInstallations (~> 12.9.0)
|
|
||||||
- Firebase/Messaging (12.9.0):
|
|
||||||
- Firebase/CoreOnly
|
|
||||||
- FirebaseMessaging (~> 12.9.0)
|
|
||||||
- firebase_app_installations (0.4.1):
|
|
||||||
- Firebase/Installations (= 12.9.0)
|
|
||||||
- firebase_core
|
|
||||||
- Flutter
|
|
||||||
- firebase_core (4.6.0):
|
|
||||||
- Firebase/CoreOnly (= 12.9.0)
|
|
||||||
- Flutter
|
|
||||||
- firebase_messaging (16.1.3):
|
|
||||||
- Firebase/Messaging (= 12.9.0)
|
|
||||||
- firebase_core
|
|
||||||
- Flutter
|
|
||||||
- FirebaseAnalytics (12.9.0):
|
|
||||||
- FirebaseAnalytics/Default (= 12.9.0)
|
|
||||||
- FirebaseCore (~> 12.9.0)
|
|
||||||
- FirebaseInstallations (~> 12.9.0)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- FirebaseAnalytics/Default (12.9.0):
|
|
||||||
- FirebaseCore (~> 12.9.0)
|
|
||||||
- FirebaseInstallations (~> 12.9.0)
|
|
||||||
- GoogleAppMeasurement/Default (= 12.9.0)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- FirebaseCore (12.9.0):
|
|
||||||
- FirebaseCoreInternal (~> 12.9.0)
|
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
|
||||||
- FirebaseCoreInternal (12.9.0):
|
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
|
||||||
- FirebaseInstallations (12.9.0):
|
|
||||||
- FirebaseCore (~> 12.9.0)
|
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
|
||||||
- PromisesObjC (~> 2.4)
|
|
||||||
- FirebaseMessaging (12.9.0):
|
|
||||||
- FirebaseCore (~> 12.9.0)
|
|
||||||
- FirebaseInstallations (~> 12.9.0)
|
|
||||||
- GoogleDataTransport (~> 10.1)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
|
||||||
- GoogleUtilities/Reachability (~> 8.1)
|
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_image_compress_common (1.0.0):
|
- flutter_image_compress_common (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Mantle
|
- Mantle
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
- SDWebImageWebPCoder
|
||||||
- flutter_keyboard_visibility_temp_fork (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_local_notifications (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage_darwin (10.0.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- flutter_sharing_intent (1.0.1):
|
- flutter_sharing_intent (1.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_volume_controller (0.0.1):
|
- flutter_volume_controller (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- gal (1.0.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- google_mlkit_barcode_scanning (0.14.2):
|
- google_mlkit_barcode_scanning (0.14.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- google_mlkit_commons
|
- google_mlkit_commons
|
||||||
|
|
@ -142,33 +24,6 @@ PODS:
|
||||||
- Flutter
|
- Flutter
|
||||||
- google_mlkit_commons
|
- google_mlkit_commons
|
||||||
- GoogleMLKit/FaceDetection (~> 9.0.0)
|
- GoogleMLKit/FaceDetection (~> 9.0.0)
|
||||||
- GoogleAdsOnDeviceConversion (3.2.0):
|
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- GoogleAppMeasurement/Core (12.9.0):
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- GoogleAppMeasurement/Default (12.9.0):
|
|
||||||
- GoogleAdsOnDeviceConversion (~> 3.2.0)
|
|
||||||
- GoogleAppMeasurement/Core (= 12.9.0)
|
|
||||||
- GoogleAppMeasurement/IdentitySupport (= 12.9.0)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- GoogleAppMeasurement/IdentitySupport (12.9.0):
|
|
||||||
- GoogleAppMeasurement/Core (= 12.9.0)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- GoogleDataTransport (10.1.0):
|
- GoogleDataTransport (10.1.0):
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
|
|
@ -185,54 +40,16 @@ PODS:
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||||
- GoogleUtilities (8.1.0):
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (= 8.1.0)
|
|
||||||
- GoogleUtilities/Environment (= 8.1.0)
|
|
||||||
- GoogleUtilities/Logger (= 8.1.0)
|
|
||||||
- GoogleUtilities/MethodSwizzler (= 8.1.0)
|
|
||||||
- GoogleUtilities/Network (= 8.1.0)
|
|
||||||
- "GoogleUtilities/NSData+zlib (= 8.1.0)"
|
|
||||||
- GoogleUtilities/Privacy (= 8.1.0)
|
|
||||||
- GoogleUtilities/Reachability (= 8.1.0)
|
|
||||||
- GoogleUtilities/SwizzlerTestHelpers (= 8.1.0)
|
|
||||||
- GoogleUtilities/UserDefaults (= 8.1.0)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
|
||||||
- GoogleUtilities/Environment
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GoogleUtilities/Network
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/Environment (8.1.0):
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Logger (8.1.0):
|
- GoogleUtilities/Logger (8.1.0):
|
||||||
- GoogleUtilities/Environment
|
- GoogleUtilities/Environment
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/MethodSwizzler (8.1.0):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/Network (8.1.0):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- "GoogleUtilities/NSData+zlib"
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/Reachability
|
|
||||||
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/Privacy (8.1.0)
|
- GoogleUtilities/Privacy (8.1.0)
|
||||||
- GoogleUtilities/Reachability (8.1.0):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/SwizzlerTestHelpers (8.1.0):
|
|
||||||
- GoogleUtilities/MethodSwizzler
|
|
||||||
- GoogleUtilities/UserDefaults (8.1.0):
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GTMSessionFetcher/Core (3.5.0)
|
- GTMSessionFetcher/Core (3.5.0)
|
||||||
- image_picker_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- in_app_purchase_storekit (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- integration_test (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- libwebp (1.5.0):
|
- libwebp (1.5.0):
|
||||||
- libwebp/demux (= 1.5.0)
|
- libwebp/demux (= 1.5.0)
|
||||||
- libwebp/mux (= 1.5.0)
|
- libwebp/mux (= 1.5.0)
|
||||||
|
|
@ -245,9 +62,6 @@ PODS:
|
||||||
- libwebp/sharpyuv (1.5.0)
|
- libwebp/sharpyuv (1.5.0)
|
||||||
- libwebp/webp (1.5.0):
|
- libwebp/webp (1.5.0):
|
||||||
- libwebp/sharpyuv
|
- libwebp/sharpyuv
|
||||||
- local_auth_darwin (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- Mantle (2.2.0):
|
- Mantle (2.2.0):
|
||||||
- Mantle/extobjc (= 2.2.0)
|
- Mantle/extobjc (= 2.2.0)
|
||||||
- Mantle/extobjc (2.2.0)
|
- Mantle/extobjc (2.2.0)
|
||||||
|
|
@ -276,18 +90,11 @@ PODS:
|
||||||
- nanopb/encode (= 3.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
- nanopb/decode (3.30910.0)
|
- nanopb/decode (3.30910.0)
|
||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- package_info_plus (0.4.5):
|
|
||||||
- Flutter
|
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (3.9.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- pro_video_editor (0.0.1):
|
- pro_video_editor (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- restart_app (1.7.3):
|
|
||||||
- Flutter
|
|
||||||
- rust_lib_twonly (0.0.1):
|
- rust_lib_twonly (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- screen_protector (1.5.1):
|
- screen_protector (1.5.1):
|
||||||
|
|
@ -300,90 +107,29 @@ PODS:
|
||||||
- SDWebImageWebPCoder (0.15.0):
|
- SDWebImageWebPCoder (0.15.0):
|
||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.17)
|
- SDWebImage/Core (~> 5.17)
|
||||||
- Sentry/HybridSDK (8.58.0)
|
- SwiftProtobuf (1.38.0)
|
||||||
- sentry_flutter (9.16.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- Sentry/HybridSDK (= 8.58.0)
|
|
||||||
- share_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- sqflite_darwin (0.0.4):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- SwiftProtobuf (1.36.1)
|
|
||||||
- SwiftyGif (5.4.5)
|
|
||||||
- url_launcher_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- video_player_avfoundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- workmanager_apple (0.0.1):
|
- workmanager_apple (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
|
||||||
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
|
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
|
||||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
|
||||||
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
|
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
|
||||||
- cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`)
|
- cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
|
||||||
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
|
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
|
||||||
- Firebase
|
|
||||||
- firebase_app_installations (from `.symlinks/plugins/firebase_app_installations/ios`)
|
|
||||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
|
||||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
|
||||||
- FirebaseCore
|
|
||||||
- FirebaseCoreInternal
|
|
||||||
- FirebaseMessaging
|
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
|
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
|
||||||
- flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`)
|
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
|
||||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
|
||||||
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
|
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
|
||||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
|
||||||
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
|
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
|
||||||
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
||||||
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`)
|
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`)
|
||||||
- GoogleUtilities
|
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
|
||||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
|
||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
|
|
||||||
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`)
|
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`)
|
||||||
- restart_app (from `.symlinks/plugins/restart_app/ios`)
|
|
||||||
- rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`)
|
- rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`)
|
||||||
- screen_protector (from `.symlinks/plugins/screen_protector/ios`)
|
- screen_protector (from `.symlinks/plugins/screen_protector/ios`)
|
||||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
|
||||||
- SwiftProtobuf
|
- SwiftProtobuf
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
|
||||||
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
|
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- DKImagePickerController
|
|
||||||
- DKPhotoGallery
|
|
||||||
- Firebase
|
|
||||||
- FirebaseAnalytics
|
|
||||||
- FirebaseCore
|
|
||||||
- FirebaseCoreInternal
|
|
||||||
- FirebaseInstallations
|
|
||||||
- FirebaseMessaging
|
|
||||||
- GoogleAdsOnDeviceConversion
|
|
||||||
- GoogleAppMeasurement
|
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleMLKit
|
- GoogleMLKit
|
||||||
- GoogleToolboxForMac
|
- GoogleToolboxForMac
|
||||||
|
|
@ -401,138 +147,54 @@ SPEC REPOS:
|
||||||
- ScreenProtectorKit
|
- ScreenProtectorKit
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
- SDWebImageWebPCoder
|
||||||
- Sentry
|
|
||||||
- SwiftProtobuf
|
- SwiftProtobuf
|
||||||
- SwiftyGif
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
|
||||||
:path: ".symlinks/plugins/app_links/ios"
|
|
||||||
audio_waveforms:
|
audio_waveforms:
|
||||||
:path: ".symlinks/plugins/audio_waveforms/ios"
|
:path: ".symlinks/plugins/audio_waveforms/ios"
|
||||||
background_downloader:
|
|
||||||
:path: ".symlinks/plugins/background_downloader/ios"
|
|
||||||
camera_avfoundation:
|
|
||||||
:path: ".symlinks/plugins/camera_avfoundation/ios"
|
|
||||||
connectivity_plus:
|
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
|
||||||
cryptography_flutter_plus:
|
cryptography_flutter_plus:
|
||||||
:path: ".symlinks/plugins/cryptography_flutter_plus/ios"
|
:path: ".symlinks/plugins/cryptography_flutter_plus/ios"
|
||||||
device_info_plus:
|
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
|
||||||
emoji_picker_flutter:
|
|
||||||
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
|
|
||||||
file_picker:
|
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
|
||||||
firebase_app_installations:
|
|
||||||
:path: ".symlinks/plugins/firebase_app_installations/ios"
|
|
||||||
firebase_core:
|
|
||||||
:path: ".symlinks/plugins/firebase_core/ios"
|
|
||||||
firebase_messaging:
|
|
||||||
:path: ".symlinks/plugins/firebase_messaging/ios"
|
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_image_compress_common:
|
flutter_image_compress_common:
|
||||||
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
|
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
|
||||||
flutter_keyboard_visibility_temp_fork:
|
|
||||||
:path: ".symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios"
|
|
||||||
flutter_local_notifications:
|
|
||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
|
||||||
flutter_secure_storage_darwin:
|
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
|
||||||
flutter_sharing_intent:
|
flutter_sharing_intent:
|
||||||
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
|
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
|
||||||
flutter_volume_controller:
|
flutter_volume_controller:
|
||||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||||
gal:
|
|
||||||
:path: ".symlinks/plugins/gal/darwin"
|
|
||||||
google_mlkit_barcode_scanning:
|
google_mlkit_barcode_scanning:
|
||||||
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
|
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
|
||||||
google_mlkit_commons:
|
google_mlkit_commons:
|
||||||
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
||||||
google_mlkit_face_detection:
|
google_mlkit_face_detection:
|
||||||
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
||||||
image_picker_ios:
|
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
|
||||||
in_app_purchase_storekit:
|
|
||||||
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
|
|
||||||
integration_test:
|
|
||||||
:path: ".symlinks/plugins/integration_test/ios"
|
|
||||||
local_auth_darwin:
|
|
||||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
|
||||||
package_info_plus:
|
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
photo_manager:
|
|
||||||
:path: ".symlinks/plugins/photo_manager/darwin"
|
|
||||||
pro_video_editor:
|
pro_video_editor:
|
||||||
:path: ".symlinks/plugins/pro_video_editor/ios"
|
:path: ".symlinks/plugins/pro_video_editor/ios"
|
||||||
restart_app:
|
|
||||||
:path: ".symlinks/plugins/restart_app/ios"
|
|
||||||
rust_lib_twonly:
|
rust_lib_twonly:
|
||||||
:path: ".symlinks/plugins/rust_lib_twonly/ios"
|
:path: ".symlinks/plugins/rust_lib_twonly/ios"
|
||||||
screen_protector:
|
screen_protector:
|
||||||
:path: ".symlinks/plugins/screen_protector/ios"
|
:path: ".symlinks/plugins/screen_protector/ios"
|
||||||
sentry_flutter:
|
|
||||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
|
||||||
share_plus:
|
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
|
||||||
shared_preferences_foundation:
|
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
|
||||||
sqflite_darwin:
|
|
||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
|
||||||
url_launcher_ios:
|
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
|
||||||
video_player_avfoundation:
|
|
||||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
|
||||||
workmanager_apple:
|
workmanager_apple:
|
||||||
:path: ".symlinks/plugins/workmanager_apple/ios"
|
:path: ".symlinks/plugins/workmanager_apple/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
|
|
||||||
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
||||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
|
||||||
camera_avfoundation: 968a9a5323c79a99c166ad9d7866bfd2047b5a9b
|
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
|
||||||
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
|
||||||
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
|
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
|
||||||
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
|
|
||||||
firebase_app_installations: 1abd8d071ea2022d7888f7a9713710c37136ff91
|
|
||||||
firebase_core: 8e6f58412ca227827c366b92e7cee047a2148c60
|
|
||||||
firebase_messaging: c3aa897e0d40109cfb7927c40dc0dea799863f3b
|
|
||||||
FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
|
|
||||||
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
|
|
||||||
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
|
|
||||||
FirebaseInstallations: 7b64ffd006032b2b019a59b803858df5112d9eaa
|
|
||||||
FirebaseMessaging: 7d6cdbff969127c4151c824fe432f0e301210f15
|
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||||
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
|
|
||||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
|
||||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
|
||||||
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
|
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
|
||||||
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
|
||||||
google_mlkit_barcode_scanning: 12d8422d8f7b00726dedf9cac00188a2b98750c2
|
google_mlkit_barcode_scanning: 12d8422d8f7b00726dedf9cac00188a2b98750c2
|
||||||
google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166
|
google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166
|
||||||
google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36
|
google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36
|
||||||
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
|
||||||
GoogleAppMeasurement: fce7c1c90640d2f9f5c56771f71deacb2ba3f98c
|
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
|
GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
|
||||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
|
||||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
|
||||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
|
||||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||||
MLImage: 0de5c6c2bf9e93b80ef752e2797f0836f03b58c0
|
MLImage: 0de5c6c2bf9e93b80ef752e2797f0836f03b58c0
|
||||||
MLKitBarcodeScanning: 39de223e7b1b8a8fbf10816a536dd292d8a39343
|
MLKitBarcodeScanning: 39de223e7b1b8a8fbf10816a536dd292d8a39343
|
||||||
|
|
@ -540,28 +202,17 @@ SPEC CHECKSUMS:
|
||||||
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
|
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
|
||||||
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
|
||||||
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
|
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
|
|
||||||
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
||||||
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
||||||
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
|
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
|
||||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||||
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
||||||
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
|
SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb
|
||||||
sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
|
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
|
||||||
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
|
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
|
||||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
|
||||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||||
|
|
||||||
PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7
|
PODFILE CHECKSUM: 245e6d5f26c858edb6b99a7d972cc93ead4d55cf
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; };
|
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; };
|
||||||
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
|
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
|
@ -114,6 +115,7 @@
|
||||||
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
@ -165,6 +167,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */,
|
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */,
|
||||||
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */,
|
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
|
|
@ -200,6 +203,7 @@
|
||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
|
@ -307,6 +311,9 @@
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
|
@ -378,6 +385,9 @@
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
|
@ -1309,6 +1319,18 @@
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "abseil-cpp-binary",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
|
||||||
|
"version" : "1.2024072200.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "app-check",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/app-check.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
|
||||||
|
"version" : "11.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "firebase-ios-sdk",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
|
||||||
|
"version" : "12.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "flutterfire",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/flutterfire",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81",
|
||||||
|
"version" : "4.6.0-firebase-core-swift"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
|
||||||
|
"version" : "3.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "googleappmeasurement",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
|
||||||
|
"version" : "12.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "googledatatransport",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||||
|
"version" : "10.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "googleutilities",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
|
||||||
|
"version" : "8.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "grpc-binary",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/grpc-binary.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
|
||||||
|
"version" : "1.69.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "gtm-session-fetcher",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||||
|
"version" : "5.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "interop-ios-for-google-sdks",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||||
|
"version" : "101.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "leveldb",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/leveldb.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||||
|
"version" : "1.22.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "nanopb",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/nanopb.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||||
|
"version" : "2.30910.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "promises",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/promises.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||||
|
"version" : "2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sentry-cocoa",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
|
||||||
|
"version" : "8.58.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,24 @@
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|
|
||||||
194
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
194
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "abseil-cpp-binary",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
|
||||||
|
"version" : "1.2024072200.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "app-check",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/app-check.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
|
||||||
|
"version" : "11.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "firebase-ios-sdk",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
|
||||||
|
"version" : "12.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "flutterfire",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/flutterfire",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81",
|
||||||
|
"version" : "4.6.0-firebase-core-swift"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
|
||||||
|
"version" : "3.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "googleappmeasurement",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
|
||||||
|
"version" : "12.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "googledatatransport",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||||
|
"version" : "10.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "googleutilities",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
|
||||||
|
"version" : "8.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "grpc-binary",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/grpc-binary.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
|
||||||
|
"version" : "1.69.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "gtm-session-fetcher",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||||
|
"version" : "5.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "interop-ios-for-google-sdks",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||||
|
"version" : "101.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "leveldb",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/leveldb.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||||
|
"version" : "1.22.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "nanopb",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/firebase/nanopb.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||||
|
"version" : "2.30910.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "promises",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/promises.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||||
|
"version" : "2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sentry-cocoa",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
|
||||||
|
"version" : "8.58.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
|
|
@ -55,6 +55,8 @@
|
||||||
<string>Use your microphone to enable audio when making videos.</string>
|
<string>Use your microphone to enable audio when making videos.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>twonly will save photos or videos to your library.</string>
|
<string>twonly will save photos or videos to your library.</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>This app does not use or store your location information.</string>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,8 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
_isTwonlyLocked = false;
|
_isTwonlyLocked = false;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) {
|
} else if (!userService.currentUser.skipSetupPages &&
|
||||||
|
userService.currentUser.currentSetupPage != null) {
|
||||||
// This will only be shown in case the user have not skipped
|
// This will only be shown in case the user have not skipped
|
||||||
child = SetupView(
|
child = SetupView(
|
||||||
onUpdate: () => setState(() {
|
onUpdate: () => setState(() {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
|
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
|
||||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
|
||||||
|
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`
|
||||||
|
|
||||||
Future<void> initFlutterCallbacks({
|
Future<void> initFlutterCallbacks({
|
||||||
|
required int callbackId,
|
||||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
||||||
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
||||||
|
|
@ -39,6 +41,7 @@ Future<void> initFlutterCallbacks({
|
||||||
required FutureOr<Uint8List?> Function(PlatformInt64)
|
required FutureOr<Uint8List?> Function(PlatformInt64)
|
||||||
userDiscoveryGetContactPromotion,
|
userDiscoveryGetContactPromotion,
|
||||||
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
||||||
|
callbackId: callbackId,
|
||||||
loggingGetStreamSink: loggingGetStreamSink,
|
loggingGetStreamSink: loggingGetStreamSink,
|
||||||
userDiscoverySignData: userDiscoverySignData,
|
userDiscoverySignData: userDiscoverySignData,
|
||||||
userDiscoveryVerifySignature: userDiscoveryVerifySignature,
|
userDiscoveryVerifySignature: userDiscoveryVerifySignature,
|
||||||
|
|
|
||||||
|
|
@ -9,36 +9,45 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
class FlutterUserDiscovery {
|
class FlutterUserDiscovery {
|
||||||
const FlutterUserDiscovery();
|
const FlutterUserDiscovery();
|
||||||
|
|
||||||
static Future<Uint8List> getCurrentVersion() => RustLib.instance.api
|
static Future<Uint8List> getCurrentVersion({required int callbackId}) =>
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion();
|
RustLib.instance.api
|
||||||
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(
|
||||||
|
callbackId: callbackId,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<List<Uint8List>> getNewMessages({
|
static Future<List<Uint8List>> getNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
required List<int> receivedVersion,
|
required List<int> receivedVersion,
|
||||||
}) => RustLib.instance.api
|
}) => RustLib.instance.api
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
|
||||||
|
callbackId: callbackId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
receivedVersion: receivedVersion,
|
receivedVersion: receivedVersion,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<void> handleNewMessages({
|
static Future<void> handleNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
required List<Uint8List> messages,
|
required List<Uint8List> messages,
|
||||||
}) => RustLib.instance.api
|
}) => RustLib.instance.api
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
|
||||||
|
callbackId: callbackId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<void> initializeOrUpdate({
|
static Future<void> initializeOrUpdate({
|
||||||
|
required int callbackId,
|
||||||
required int threshold,
|
required int threshold,
|
||||||
required PlatformInt64 userId,
|
required PlatformInt64 userId,
|
||||||
required List<int> publicKey,
|
required List<int> publicKey,
|
||||||
required bool sharePromotion,
|
required bool sharePromotion,
|
||||||
}) => RustLib.instance.api
|
}) => RustLib.instance.api
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
|
||||||
|
callbackId: callbackId,
|
||||||
threshold: threshold,
|
threshold: threshold,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
|
|
@ -46,19 +55,23 @@ class FlutterUserDiscovery {
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<Uint8List?> shouldRequestNewMessages({
|
static Future<Uint8List?> shouldRequestNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
required List<int> version,
|
required List<int> version,
|
||||||
}) => RustLib.instance.api
|
}) => RustLib.instance.api
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
|
||||||
|
callbackId: callbackId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
version: version,
|
version: version,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<void> updateVerificationStateForUser({
|
static Future<void> updateVerificationStateForUser({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
}) => RustLib.instance.api
|
}) => RustLib.instance.api
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
|
||||||
|
callbackId: callbackId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -87,16 +87,20 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
|
|
||||||
abstract class RustLibApi extends BaseApi {
|
abstract class RustLibApi extends BaseApi {
|
||||||
Future<Uint8List>
|
Future<Uint8List>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion();
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({
|
||||||
|
required int callbackId,
|
||||||
|
});
|
||||||
|
|
||||||
Future<List<Uint8List>>
|
Future<List<Uint8List>>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
required List<int> receivedVersion,
|
required List<int> receivedVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
required List<Uint8List> messages,
|
required List<Uint8List> messages,
|
||||||
|
|
@ -104,6 +108,7 @@ abstract class RustLibApi extends BaseApi {
|
||||||
|
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
|
||||||
|
required int callbackId,
|
||||||
required int threshold,
|
required int threshold,
|
||||||
required PlatformInt64 userId,
|
required PlatformInt64 userId,
|
||||||
required List<int> publicKey,
|
required List<int> publicKey,
|
||||||
|
|
@ -112,17 +117,20 @@ abstract class RustLibApi extends BaseApi {
|
||||||
|
|
||||||
Future<Uint8List?>
|
Future<Uint8List?>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
required List<int> version,
|
required List<int> version,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
||||||
|
required int callbackId,
|
||||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
||||||
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
||||||
|
|
@ -242,11 +250,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List>
|
Future<Uint8List>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion() {
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({
|
||||||
|
required int callbackId,
|
||||||
|
}) {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_u_32(callbackId, serializer);
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
|
|
@ -260,7 +271,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta:
|
constMeta:
|
||||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta,
|
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta,
|
||||||
argValues: [],
|
argValues: [callbackId],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -270,12 +281,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta =>
|
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta =>
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "flutter_user_discovery_get_current_version",
|
debugName: "flutter_user_discovery_get_current_version",
|
||||||
argNames: [],
|
argNames: ["callbackId"],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Uint8List>>
|
Future<List<Uint8List>>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
required List<int> receivedVersion,
|
required List<int> receivedVersion,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -283,6 +295,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_u_32(callbackId, serializer);
|
||||||
sse_encode_i_64(contactId, serializer);
|
sse_encode_i_64(contactId, serializer);
|
||||||
sse_encode_list_prim_u_8_loose(receivedVersion, serializer);
|
sse_encode_list_prim_u_8_loose(receivedVersion, serializer);
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
|
|
@ -298,7 +311,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta:
|
constMeta:
|
||||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta,
|
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta,
|
||||||
argValues: [contactId, receivedVersion],
|
argValues: [callbackId, contactId, receivedVersion],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -308,12 +321,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta =>
|
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta =>
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "flutter_user_discovery_get_new_messages",
|
debugName: "flutter_user_discovery_get_new_messages",
|
||||||
argNames: ["contactId", "receivedVersion"],
|
argNames: ["callbackId", "contactId", "receivedVersion"],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
required List<Uint8List> messages,
|
required List<Uint8List> messages,
|
||||||
|
|
@ -322,6 +336,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_u_32(callbackId, serializer);
|
||||||
sse_encode_i_64(contactId, serializer);
|
sse_encode_i_64(contactId, serializer);
|
||||||
sse_encode_opt_box_autoadd_i_64(
|
sse_encode_opt_box_autoadd_i_64(
|
||||||
publicKeyVerifiedTimestamp,
|
publicKeyVerifiedTimestamp,
|
||||||
|
|
@ -341,7 +356,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta:
|
constMeta:
|
||||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta,
|
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta,
|
||||||
argValues: [contactId, publicKeyVerifiedTimestamp, messages],
|
argValues: [
|
||||||
|
callbackId,
|
||||||
|
contactId,
|
||||||
|
publicKeyVerifiedTimestamp,
|
||||||
|
messages,
|
||||||
|
],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -351,12 +371,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta =>
|
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta =>
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "flutter_user_discovery_handle_new_messages",
|
debugName: "flutter_user_discovery_handle_new_messages",
|
||||||
argNames: ["contactId", "publicKeyVerifiedTimestamp", "messages"],
|
argNames: [
|
||||||
|
"callbackId",
|
||||||
|
"contactId",
|
||||||
|
"publicKeyVerifiedTimestamp",
|
||||||
|
"messages",
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
|
||||||
|
required int callbackId,
|
||||||
required int threshold,
|
required int threshold,
|
||||||
required PlatformInt64 userId,
|
required PlatformInt64 userId,
|
||||||
required List<int> publicKey,
|
required List<int> publicKey,
|
||||||
|
|
@ -366,6 +392,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_u_32(callbackId, serializer);
|
||||||
sse_encode_u_8(threshold, serializer);
|
sse_encode_u_8(threshold, serializer);
|
||||||
sse_encode_i_64(userId, serializer);
|
sse_encode_i_64(userId, serializer);
|
||||||
sse_encode_list_prim_u_8_loose(publicKey, serializer);
|
sse_encode_list_prim_u_8_loose(publicKey, serializer);
|
||||||
|
|
@ -383,7 +410,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta:
|
constMeta:
|
||||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta,
|
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta,
|
||||||
argValues: [threshold, userId, publicKey, sharePromotion],
|
argValues: [callbackId, threshold, userId, publicKey, sharePromotion],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -393,12 +420,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta =>
|
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta =>
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "flutter_user_discovery_initialize_or_update",
|
debugName: "flutter_user_discovery_initialize_or_update",
|
||||||
argNames: ["threshold", "userId", "publicKey", "sharePromotion"],
|
argNames: [
|
||||||
|
"callbackId",
|
||||||
|
"threshold",
|
||||||
|
"userId",
|
||||||
|
"publicKey",
|
||||||
|
"sharePromotion",
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List?>
|
Future<Uint8List?>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
required List<int> version,
|
required List<int> version,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -406,6 +440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_u_32(callbackId, serializer);
|
||||||
sse_encode_i_64(contactId, serializer);
|
sse_encode_i_64(contactId, serializer);
|
||||||
sse_encode_list_prim_u_8_loose(version, serializer);
|
sse_encode_list_prim_u_8_loose(version, serializer);
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
|
|
@ -421,7 +456,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta:
|
constMeta:
|
||||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta,
|
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta,
|
||||||
argValues: [contactId, version],
|
argValues: [callbackId, contactId, version],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -431,12 +466,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta =>
|
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta =>
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "flutter_user_discovery_should_request_new_messages",
|
debugName: "flutter_user_discovery_should_request_new_messages",
|
||||||
argNames: ["contactId", "version"],
|
argNames: ["callbackId", "contactId", "version"],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
|
||||||
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -444,6 +480,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_u_32(callbackId, serializer);
|
||||||
sse_encode_i_64(contactId, serializer);
|
sse_encode_i_64(contactId, serializer);
|
||||||
sse_encode_opt_box_autoadd_i_64(
|
sse_encode_opt_box_autoadd_i_64(
|
||||||
publicKeyVerifiedTimestamp,
|
publicKeyVerifiedTimestamp,
|
||||||
|
|
@ -462,7 +499,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta:
|
constMeta:
|
||||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta,
|
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta,
|
||||||
argValues: [contactId, publicKeyVerifiedTimestamp],
|
argValues: [callbackId, contactId, publicKeyVerifiedTimestamp],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -472,11 +509,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta =>
|
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta =>
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "flutter_user_discovery_update_verification_state_for_user",
|
debugName: "flutter_user_discovery_update_verification_state_for_user",
|
||||||
argNames: ["contactId", "publicKeyVerifiedTimestamp"],
|
argNames: ["callbackId", "contactId", "publicKeyVerifiedTimestamp"],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
||||||
|
required int callbackId,
|
||||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
||||||
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
||||||
|
|
@ -513,6 +551,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_u_32(callbackId, serializer);
|
||||||
sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
|
sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
|
||||||
loggingGetStreamSink,
|
loggingGetStreamSink,
|
||||||
serializer,
|
serializer,
|
||||||
|
|
@ -586,6 +625,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta: kCrateBridgeCallbacksInitFlutterCallbacksConstMeta,
|
constMeta: kCrateBridgeCallbacksInitFlutterCallbacksConstMeta,
|
||||||
argValues: [
|
argValues: [
|
||||||
|
callbackId,
|
||||||
loggingGetStreamSink,
|
loggingGetStreamSink,
|
||||||
userDiscoverySignData,
|
userDiscoverySignData,
|
||||||
userDiscoveryVerifySignature,
|
userDiscoveryVerifySignature,
|
||||||
|
|
@ -611,6 +651,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "init_flutter_callbacks",
|
debugName: "init_flutter_callbacks",
|
||||||
argNames: [
|
argNames: [
|
||||||
|
"callbackId",
|
||||||
"loggingGetStreamSink",
|
"loggingGetStreamSink",
|
||||||
"userDiscoverySignData",
|
"userDiscoverySignData",
|
||||||
"userDiscoveryVerifySignature",
|
"userDiscoveryVerifySignature",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
final int isolateCallbackId = Random().nextInt(0x7FFFFFFF);
|
||||||
|
|
||||||
class AppEnvironment {
|
class AppEnvironment {
|
||||||
static late String cacheDir;
|
static late String cacheDir;
|
||||||
static late String supportDir;
|
static late String supportDir;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:twonly/core/bridge/callbacks.dart';
|
import 'package:twonly/core/bridge/callbacks.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/callbacks/logging.callbacks.dart';
|
import 'package:twonly/src/callbacks/logging.callbacks.dart';
|
||||||
import 'package:twonly/src/callbacks/user_discovery.callbacks.dart';
|
import 'package:twonly/src/callbacks/user_discovery.callbacks.dart';
|
||||||
|
|
||||||
Future<void> initFlutterCallbacksForRust() async {
|
Future<void> initFlutterCallbacksForRust() async {
|
||||||
await initFlutterCallbacks(
|
await initFlutterCallbacks(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
|
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
|
||||||
userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
|
userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
|
||||||
userDiscoveryGetShareForContact:
|
userDiscoveryGetShareForContact:
|
||||||
|
|
|
||||||
|
|
@ -62,5 +62,7 @@ class Routes {
|
||||||
'/settings/developer/automated_testing';
|
'/settings/developer/automated_testing';
|
||||||
static const String settingsDeveloperReduceFlames =
|
static const String settingsDeveloperReduceFlames =
|
||||||
'/settings/developer/reduce_flames';
|
'/settings/developer/reduce_flames';
|
||||||
|
static const String settingsDeveloperInformations =
|
||||||
|
'/settings/developer/informations';
|
||||||
static const String settingsInvite = '/settings/invite';
|
static const String settingsInvite = '/settings/invite';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,7 @@ class SecureStorageKeys {
|
||||||
// Not required for backup...
|
// Not required for backup...
|
||||||
static const String receivingPushKeys = 'push_keys_receiving';
|
static const String receivingPushKeys = 'push_keys_receiving';
|
||||||
static const String sendingPushKeys = 'push_keys_sending';
|
static const String sendingPushKeys = 'push_keys_sending';
|
||||||
|
static const String lastFcmMessageTimestamp = 'last_fcm_message_timestamp';
|
||||||
|
static const String lastServerMessageTimestamp =
|
||||||
|
'last_server_message_timestamp';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:clock/clock.dart' show clock;
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hashlib/random.dart';
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
|
@ -327,12 +328,16 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
String groupId,
|
String groupId,
|
||||||
DateTime newLastMessage,
|
DateTime newLastMessage,
|
||||||
) async {
|
) async {
|
||||||
|
final now = clock.now();
|
||||||
|
final clampedLastMessage = newLastMessage.isAfter(now)
|
||||||
|
? now
|
||||||
|
: newLastMessage;
|
||||||
await (update(groups)..where(
|
await (update(groups)..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.groupId.equals(groupId) &
|
t.groupId.equals(groupId) &
|
||||||
(t.lastMessageExchange.isSmallerThanValue(newLastMessage)),
|
(t.lastMessageExchange.isSmallerThanValue(clampedLastMessage)),
|
||||||
))
|
))
|
||||||
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage)));
|
.write(GroupsCompanion(lastMessageExchange: Value(clampedLastMessage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {
|
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
|
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
|
|
@ -216,6 +217,7 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
);
|
);
|
||||||
if (userService.currentUser.isUserDiscoveryEnabled) {
|
if (userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
await FlutterUserDiscovery.updateVerificationStateForUser(
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
|
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
|
||||||
);
|
);
|
||||||
|
|
@ -232,6 +234,7 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
)..where((kv) => kv.contactId.equals(contactId))).go();
|
)..where((kv) => kv.contactId.equals(contactId))).go();
|
||||||
if (userService.currentUser.isUserDiscoveryEnabled) {
|
if (userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
await FlutterUserDiscovery.updateVerificationStateForUser(
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -251,6 +254,7 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
final remaining = await getContactVerification(contactId);
|
final remaining = await getContactVerification(contactId);
|
||||||
if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
|
if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
await FlutterUserDiscovery.updateVerificationStateForUser(
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
|
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
|
||||||
final query =
|
final query =
|
||||||
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
|
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
|
||||||
..groupBy([mediaFiles.storedFileHash]);
|
..groupBy([
|
||||||
|
const CustomExpression<Object>('COALESCE(stored_file_hash, media_id)')
|
||||||
|
]);
|
||||||
return query.map((row) => row.readTable(mediaFiles)).watch();
|
return query.map((row) => row.readTable(mediaFiles)).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
mediaFiles,
|
mediaFiles,
|
||||||
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||||
),
|
),
|
||||||
])..where(
|
])
|
||||||
|
..where(
|
||||||
mediaFiles.downloadState
|
mediaFiles.downloadState
|
||||||
.equals(DownloadState.reuploadRequested.name)
|
.equals(DownloadState.reuploadRequested.name)
|
||||||
.not() &
|
.not() &
|
||||||
|
|
@ -60,7 +61,8 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
messages.mediaId.isNotNull() &
|
messages.mediaId.isNotNull() &
|
||||||
messages.senderId.isNotNull() &
|
messages.senderId.isNotNull() &
|
||||||
messages.type.equals(MessageType.media.name),
|
messages.type.equals(MessageType.media.name),
|
||||||
);
|
)
|
||||||
|
..orderBy([OrderingTerm.asc(messages.createdAt)]);
|
||||||
return query.map((row) => row.readTable(messages)).watch();
|
return query.map((row) => row.readTable(messages)).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,24 +95,33 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
milliseconds: group!.deleteMessagesAfterMilliseconds,
|
milliseconds: group!.deleteMessagesAfterMilliseconds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return ((select(messages)..where(
|
final query =
|
||||||
(t) =>
|
select(messages).join([
|
||||||
t.groupId.equals(groupId) &
|
leftOuterJoin(
|
||||||
// messages in groups will only be removed in case all members have received it...
|
mediaFiles,
|
||||||
// so ensuring that this message is not shown in the messages anymore
|
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||||
(t.openedAt.isBiggerThanValue(deletionTime) |
|
),
|
||||||
t.openedAt.isNull() |
|
])
|
||||||
t.mediaStored.equals(true)) &
|
..where(
|
||||||
(t.isDeletedFromSender.equals(true) |
|
messages.groupId.equals(groupId) &
|
||||||
(t.type.equals(MessageType.text.name).not() &
|
(messages.openedAt.isBiggerThanValue(deletionTime) |
|
||||||
t.type.equals(MessageType.media.name).not()) |
|
messages.openedAt.isNull() |
|
||||||
(t.type.equals(MessageType.text.name) &
|
messages.mediaStored.equals(true)) &
|
||||||
t.content.isNotNull()) |
|
(messages.isDeletedFromSender.equals(true) |
|
||||||
(t.type.equals(MessageType.media.name) &
|
(messages.type.equals(MessageType.text.name).not() &
|
||||||
t.mediaId.isNotNull())),
|
messages.type.equals(MessageType.media.name).not()) |
|
||||||
))
|
(messages.type.equals(MessageType.text.name) &
|
||||||
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
messages.content.isNotNull()) |
|
||||||
.watch();
|
(messages.type.equals(MessageType.media.name) &
|
||||||
|
messages.mediaId.isNotNull() &
|
||||||
|
(mediaFiles.downloadState.isNull() |
|
||||||
|
mediaFiles.downloadState
|
||||||
|
.equals(DownloadState.reuploadRequested.name)
|
||||||
|
.not()))),
|
||||||
|
)
|
||||||
|
..orderBy([OrderingTerm.asc(messages.createdAt)]);
|
||||||
|
|
||||||
|
return query.map((row) => row.readTable(messages)).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<(GroupMember, Contact)>> watchMembersByGroupId(String groupId) {
|
Stream<List<(GroupMember, Contact)>> watchMembersByGroupId(String groupId) {
|
||||||
|
|
@ -161,7 +172,8 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
m.isDeletedFromSender.equals(true)) |
|
m.isDeletedFromSender.equals(true)) |
|
||||||
m.mediaStored.equals(false)) &
|
m.mediaStored.equals(false)) &
|
||||||
// Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later..
|
// Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later..
|
||||||
(m.openedByAll.isSmallerThanValue(deletionTime) |
|
((m.openedByAll.isNotNull() &
|
||||||
|
m.openedByAll.isSmallerThanValue(deletionTime)) |
|
||||||
(m.isDeletedFromSender.equals(true) &
|
(m.isDeletedFromSender.equals(true) &
|
||||||
m.createdAt.isSmallerThanValue(deletionTime))),
|
m.createdAt.isSmallerThanValue(deletionTime))),
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,11 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
}
|
}
|
||||||
|
|
||||||
void markUpdated() {
|
void markUpdated() {
|
||||||
notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)});
|
notifyUpdates({
|
||||||
notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)});
|
TableUpdate.onTable(messages, kind: UpdateKind.update),
|
||||||
|
TableUpdate.onTable(contacts, kind: UpdateKind.update),
|
||||||
|
TableUpdate.onTable(groups, kind: UpdateKind.update),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> printTableSizes() async {
|
Future<void> printTableSizes() async {
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @registerSlogan.
|
/// No description provided for @registerSlogan.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Stay in touch with friends privately and securely.'**
|
/// **'Stay in touch privately.'**
|
||||||
String get registerSlogan;
|
String get registerSlogan;
|
||||||
|
|
||||||
/// No description provided for @onboardingWelcomeTitle.
|
/// No description provided for @onboardingWelcomeTitle.
|
||||||
|
|
@ -173,7 +173,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @registerUsernameSlogan.
|
/// No description provided for @registerUsernameSlogan.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Your public username'**
|
/// **'Create your account'**
|
||||||
String get registerUsernameSlogan;
|
String get registerUsernameSlogan;
|
||||||
|
|
||||||
/// No description provided for @registerUsernameDecoration.
|
/// No description provided for @registerUsernameDecoration.
|
||||||
|
|
@ -1205,8 +1205,8 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @userFoundBody.
|
/// No description provided for @userFoundBody.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Do you want to create a follow request?'**
|
/// **'Do you want to connect with {username}?'**
|
||||||
String get userFoundBody;
|
String userFoundBody(String username);
|
||||||
|
|
||||||
/// No description provided for @errorInternalError.
|
/// No description provided for @errorInternalError.
|
||||||
///
|
///
|
||||||
|
|
@ -3689,6 +3689,48 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'{count, plural, =1{Import 1 item} other{Import {count} items}}'**
|
/// **'{count, plural, =1{Import 1 item} other{Import {count} items}}'**
|
||||||
String importGalleryImportCount(num count);
|
String importGalleryImportCount(num count);
|
||||||
|
|
||||||
|
/// No description provided for @emptyChatListTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Find your first friend'**
|
||||||
|
String get emptyChatListTitle;
|
||||||
|
|
||||||
|
/// No description provided for @emptyChatListDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Let friends scan your QR code, or share them your profile.'**
|
||||||
|
String get emptyChatListDesc;
|
||||||
|
|
||||||
|
/// No description provided for @emptyChatListShareBtn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Share your profile'**
|
||||||
|
String get emptyChatListShareBtn;
|
||||||
|
|
||||||
|
/// No description provided for @emptyChatListScanBtn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'QR Code'**
|
||||||
|
String get emptyChatListScanBtn;
|
||||||
|
|
||||||
|
/// No description provided for @emptyChatListAddUsernameBtn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'By Username'**
|
||||||
|
String get emptyChatListAddUsernameBtn;
|
||||||
|
|
||||||
|
/// No description provided for @avatarCustomizeRandomize.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Randomize'**
|
||||||
|
String get avatarCustomizeRandomize;
|
||||||
|
|
||||||
|
/// No description provided for @avatarCustomizeReset.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reset'**
|
||||||
|
String get avatarCustomizeReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerSlogan =>
|
String get registerSlogan => 'Privat in Kontakt bleiben.';
|
||||||
'Privat und sicher mit Freunden in Kontakt bleiben.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingWelcomeTitle => 'Willkommen bei twonly!';
|
String get onboardingWelcomeTitle => 'Willkommen bei twonly!';
|
||||||
|
|
@ -52,7 +51,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get onboardingGetStartedTitle => 'Auf geht\'s';
|
String get onboardingGetStartedTitle => 'Auf geht\'s';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameSlogan => 'Dein öffentlicher Benutzername';
|
String get registerUsernameSlogan => 'Konto erstellen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameDecoration => 'Benutzername';
|
String get registerUsernameDecoration => 'Benutzername';
|
||||||
|
|
@ -611,7 +610,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userFoundBody => 'Möchtest du eine Folgeanfrage stellen?';
|
String userFoundBody(String username) {
|
||||||
|
return 'Möchtest du dich mit $username vernetzen?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get errorInternalError =>
|
String get errorInternalError =>
|
||||||
|
|
@ -2127,4 +2128,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListTitle => 'Finde deinen ersten Freund';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListDesc =>
|
||||||
|
'Lass Freunde deinen QR-Code scannen oder teile dein Profil mit ihnen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListShareBtn => 'Profil teilen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListScanBtn => 'QR-Code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListAddUsernameBtn => 'Per Benutzername';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get avatarCustomizeRandomize => 'Zufällig';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerSlogan =>
|
String get registerSlogan => 'Stay in touch privately.';
|
||||||
'Stay in touch with friends privately and securely.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingWelcomeTitle => 'Welcome to twonly!';
|
String get onboardingWelcomeTitle => 'Welcome to twonly!';
|
||||||
|
|
@ -51,7 +50,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get onboardingGetStartedTitle => 'Let\'s go!';
|
String get onboardingGetStartedTitle => 'Let\'s go!';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameSlogan => 'Your public username';
|
String get registerUsernameSlogan => 'Create your account';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameDecoration => 'Username';
|
String get registerUsernameDecoration => 'Username';
|
||||||
|
|
@ -606,7 +605,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userFoundBody => 'Do you want to create a follow request?';
|
String userFoundBody(String username) {
|
||||||
|
return 'Do you want to connect with $username?';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get errorInternalError =>
|
String get errorInternalError =>
|
||||||
|
|
@ -2109,4 +2110,26 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListTitle => 'Find your first friend';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListDesc =>
|
||||||
|
'Let friends scan your QR code, or share them your profile.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListShareBtn => 'Share your profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListScanBtn => 'QR Code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get emptyChatListAddUsernameBtn => 'By Username';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get avatarCustomizeRandomize => 'Randomize';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get avatarCustomizeReset => 'Reset';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 189bf8f4dbe2bee4f19a15b9640b8826e4f2e235
|
Subproject commit c95e98ca929d630ead028d84e13934b30dbeba3b
|
||||||
|
|
@ -114,8 +114,8 @@ class UserData {
|
||||||
@JsonKey(defaultValue: 4)
|
@JsonKey(defaultValue: 4)
|
||||||
int requiredSendImages = 4;
|
int requiredSendImages = 4;
|
||||||
|
|
||||||
@JsonKey(defaultValue: 2)
|
@JsonKey(defaultValue: 3)
|
||||||
int userDiscoveryThreshold = 2;
|
int userDiscoveryThreshold = 3;
|
||||||
|
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
bool userDiscoveryRequiresManualApproval = false;
|
bool userDiscoveryRequiresManualApproval = false;
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
..storeMediaFilesInGallery =
|
..storeMediaFilesInGallery =
|
||||||
json['storeMediaFilesInGallery'] as bool? ?? false
|
json['storeMediaFilesInGallery'] as bool? ?? true
|
||||||
..autoStoreAllSendUnlimitedMediaFiles =
|
..autoStoreAllSendUnlimitedMediaFiles =
|
||||||
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
|
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
|
||||||
..typingIndicators = json['typingIndicators'] as bool? ?? true
|
..typingIndicators = json['typingIndicators'] as bool? ?? true
|
||||||
|
|
@ -78,7 +78,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
json['isUserDiscoveryEnabled'] as bool? ?? false
|
json['isUserDiscoveryEnabled'] as bool? ?? false
|
||||||
..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4
|
..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4
|
||||||
..userDiscoveryThreshold =
|
..userDiscoveryThreshold =
|
||||||
(json['userDiscoveryThreshold'] as num?)?.toInt() ?? 2
|
(json['userDiscoveryThreshold'] as num?)?.toInt() ?? 3
|
||||||
..userDiscoveryRequiresManualApproval =
|
..userDiscoveryRequiresManualApproval =
|
||||||
json['userDiscoveryRequiresManualApproval'] as bool? ?? false
|
json['userDiscoveryRequiresManualApproval'] as bool? ?? false
|
||||||
..userDiscoverySharePromotion =
|
..userDiscoverySharePromotion =
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import 'package:twonly/src/visual/views/settings/data_and_storage/import_from_ga
|
||||||
import 'package:twonly/src/visual/views/settings/data_and_storage/manage_storage.view.dart';
|
import 'package:twonly/src/visual/views/settings/data_and_storage/manage_storage.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/developer/informations.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/retransmission_data.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/retransmission_data.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/changelog.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/changelog.view.dart';
|
||||||
|
|
@ -288,6 +289,10 @@ final routerProvider = GoRouter(
|
||||||
path: 'automated_testing',
|
path: 'automated_testing',
|
||||||
builder: (context, state) => const AutomatedTestingView(),
|
builder: (context, state) => const AutomatedTestingView(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'informations',
|
||||||
|
builder: (context, state) => const DeveloperInformationsView(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'reduce_flames',
|
path: 'reduce_flames',
|
||||||
builder: (context, state) => const ReduceFlamesView(),
|
builder: (context, state) => const ReduceFlamesView(),
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ Future<void> downloadFileFast(
|
||||||
} else {
|
} else {
|
||||||
if (response.statusCode == 404 || response.statusCode == 403) {
|
if (response.statusCode == 404 || response.statusCode == 403) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'Got ${response.statusCode} from server. Requesting upload again',
|
'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
|
||||||
);
|
);
|
||||||
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
||||||
await requestMediaReupload(media.mediaId);
|
await requestMediaReupload(media.mediaId);
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/group.service.dart';
|
import 'package:twonly/src/services/group.service.dart';
|
||||||
import 'package:twonly/src/services/key_verification.service.dart';
|
import 'package:twonly/src/services/key_verification.service.dart';
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -153,7 +154,10 @@ Future<void> _handleClient2ClientMessage(
|
||||||
Log.info(
|
Log.info(
|
||||||
'[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.',
|
'[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.',
|
||||||
);
|
);
|
||||||
await tryToSendCompleteMessage(receiptId: newReceiptId, blocking: false);
|
await tryToSendCompleteMessage(
|
||||||
|
receiptId: newReceiptId,
|
||||||
|
blocking: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Message_Type.CIPHERTEXT:
|
case Message_Type.CIPHERTEXT:
|
||||||
|
|
@ -276,9 +280,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
||||||
|
|
||||||
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
||||||
|
|
||||||
if (Platform.isAndroid && a == null && b == null) {
|
if (a == null && b == null) {
|
||||||
// Message was handled without any error -> Show push notification to the user.
|
unawaited(updateLastServerMessageTimestamp());
|
||||||
await showPushNotificationFromServerMessages(fromUserId, encryptedContent);
|
if (Platform.isAndroid) {
|
||||||
|
// Message was handled without any error. Show push notification to the user for Android.
|
||||||
|
await showPushNotificationFromServerMessages(
|
||||||
|
fromUserId,
|
||||||
|
encryptedContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (a, b);
|
return (a, b);
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,12 @@ class MediaFileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createThumbnail() async {
|
Future<void> createThumbnail() async {
|
||||||
if (!storedPath.existsSync()) {
|
if (!storedPath.existsSync() || storedPath.lengthSync() == 0) {
|
||||||
|
if (storedPath.existsSync() && storedPath.lengthSync() == 0) {
|
||||||
|
try {
|
||||||
|
storedPath.deleteSync();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
if (mediaFile.stored &&
|
if (mediaFile.stored &&
|
||||||
mediaFile.createdAt.isBefore(
|
mediaFile.createdAt.isBefore(
|
||||||
clock.now().subtract(const Duration(days: 30)),
|
clock.now().subtract(const Duration(days: 30)),
|
||||||
|
|
@ -288,8 +293,10 @@ class MediaFileService {
|
||||||
|
|
||||||
bool get imagePreviewAvailable =>
|
bool get imagePreviewAvailable =>
|
||||||
mediaFile.hasThumbnail ||
|
mediaFile.hasThumbnail ||
|
||||||
thumbnailPath.existsSync() ||
|
(thumbnailPath.existsSync() && thumbnailPath.lengthSync() > 0) ||
|
||||||
storedPath.existsSync();
|
mediaFile.type == MediaType.audio ||
|
||||||
|
((mediaFile.type == MediaType.image || mediaFile.type == MediaType.gif) &&
|
||||||
|
storedPath.existsSync() && storedPath.lengthSync() > 0);
|
||||||
|
|
||||||
Future<void> storeMediaFile() async {
|
Future<void> storeMediaFile() async {
|
||||||
Log.info('Storing media file ${mediaFile.mediaId}');
|
Log.info('Storing media file ${mediaFile.mediaId}');
|
||||||
|
|
@ -439,7 +446,7 @@ class MediaFileService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!storedPath.existsSync()) {
|
if (!storedPath.existsSync() || storedPath.lengthSync() == 0) {
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
mediaFile.mediaId,
|
mediaFile.mediaId,
|
||||||
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||||
|
|
@ -448,7 +455,7 @@ class MediaFileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final bytes = storedPath.readAsBytesSync();
|
final bytes = await storedPath.readAsBytes();
|
||||||
final result = await compute(_processImageCrop, bytes);
|
final result = await compute(_processImageCrop, bytes);
|
||||||
|
|
||||||
if (result.isCropped && result.pngBytes != null) {
|
if (result.isCropped && result.pngBytes != null) {
|
||||||
|
|
@ -460,18 +467,18 @@ class MediaFileService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (webpBytes.isNotEmpty) {
|
if (webpBytes.isNotEmpty) {
|
||||||
storedPath.writeAsBytesSync(webpBytes);
|
await storedPath.writeAsBytes(webpBytes);
|
||||||
} else {
|
} else {
|
||||||
Log.warn('WebP compression returned empty, falling back to PNG');
|
Log.warn('WebP compression returned empty, falling back to PNG');
|
||||||
storedPath.writeAsBytesSync(result.pngBytes!);
|
await storedPath.writeAsBytes(result.pngBytes!);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Error compressing to WebP, falling back to PNG: $e');
|
Log.error('Error compressing to WebP, falling back to PNG: $e');
|
||||||
storedPath.writeAsBytesSync(result.pngBytes!);
|
await storedPath.writeAsBytes(result.pngBytes!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbnailPath.existsSync()) {
|
if (thumbnailPath.existsSync()) {
|
||||||
thumbnailPath.deleteSync();
|
await thumbnailPath.delete();
|
||||||
}
|
}
|
||||||
await createThumbnail();
|
await createThumbnail();
|
||||||
final checksum = await sha256File(storedPath);
|
final checksum = await sha256File(storedPath);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:pro_video_editor/pro_video_editor.dart';
|
import 'package:pro_video_editor/pro_video_editor.dart';
|
||||||
|
|
@ -11,10 +12,27 @@ Future<bool> createThumbnailsForVideo(
|
||||||
) async {
|
) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
if (!sourceFile.existsSync() || sourceFile.lengthSync() == 0) {
|
||||||
|
Log.warn('Source video file does not exist or is empty.');
|
||||||
|
try {
|
||||||
if (destinationFile.existsSync()) {
|
if (destinationFile.existsSync()) {
|
||||||
return true;
|
destinationFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
if (destinationFile.lengthSync() > 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
final images = await ProVideoEditor.instance.getThumbnails(
|
final images = await ProVideoEditor.instance.getThumbnails(
|
||||||
ThumbnailConfigs(
|
ThumbnailConfigs(
|
||||||
video: EditorVideo.file(sourceFile),
|
video: EditorVideo.file(sourceFile),
|
||||||
|
|
@ -26,19 +44,29 @@ Future<bool> createThumbnailsForVideo(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (images.isNotEmpty) {
|
if (images.isNotEmpty && images.first.isNotEmpty) {
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
destinationFile.writeAsBytesSync(images.first);
|
await destinationFile.writeAsBytes(images.first);
|
||||||
|
if (destinationFile.existsSync() && destinationFile.lengthSync() > 0) {
|
||||||
Log.info(
|
Log.info(
|
||||||
'It took ${stopwatch.elapsedMilliseconds}ms to create the video thumbnail.',
|
'It took ${stopwatch.elapsedMilliseconds}ms to create the video thumbnail.',
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error creating video thumbnail: $e');
|
||||||
|
}
|
||||||
|
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'Thumbnail creation failed for the video.',
|
'Thumbnail creation failed for the video.',
|
||||||
);
|
);
|
||||||
return false;
|
try {
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
destinationFile.deleteSync();
|
||||||
}
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> createThumbnailsForImage(
|
Future<bool> createThumbnailsForImage(
|
||||||
|
|
@ -47,6 +75,26 @@ Future<bool> createThumbnailsForImage(
|
||||||
) async {
|
) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
if (!sourceFile.existsSync() || sourceFile.lengthSync() == 0) {
|
||||||
|
Log.warn('Source image file does not exist or is empty.');
|
||||||
|
try {
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
if (destinationFile.lengthSync() > 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FlutterImageCompress.compressAndGetFile(
|
await FlutterImageCompress.compressAndGetFile(
|
||||||
sourceFile.absolute.path,
|
sourceFile.absolute.path,
|
||||||
|
|
@ -57,12 +105,28 @@ Future<bool> createThumbnailsForImage(
|
||||||
format: CompressFormat.webp,
|
format: CompressFormat.webp,
|
||||||
);
|
);
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
|
|
||||||
|
if (destinationFile.existsSync() && destinationFile.lengthSync() > 0) {
|
||||||
Log.info(
|
Log.info(
|
||||||
'It took ${stopwatch.elapsedMilliseconds}ms to create the image thumbnail.',
|
'It took ${stopwatch.elapsedMilliseconds}ms to create the image thumbnail.',
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.error('Compressed image thumbnail is empty or missing.');
|
||||||
|
try {
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Error creating image thumbnail: $e');
|
Log.error('Error creating image thumbnail: $e');
|
||||||
|
try {
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,40 +137,81 @@ Future<bool> createThumbnailsForGif(
|
||||||
) async {
|
) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
if (!sourceFile.existsSync() || sourceFile.lengthSync() == 0) {
|
||||||
|
Log.warn('Source GIF file does not exist or is empty.');
|
||||||
|
try {
|
||||||
if (destinationFile.existsSync()) {
|
if (destinationFile.existsSync()) {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
if (destinationFile.lengthSync() > 0) {
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For GIFs, we decode the first frame and save it as WebP
|
// For GIFs, we decode the first frame and save it as WebP
|
||||||
final bytes = sourceFile.readAsBytesSync();
|
final bytes = await sourceFile.readAsBytes();
|
||||||
final image = img.decodeGif(bytes);
|
final pngBytes = await compute(_processGifThumbnail, bytes);
|
||||||
if (image == null) {
|
if (pngBytes == null || pngBytes.isEmpty) {
|
||||||
Log.error('Could not decode GIF for thumbnail.');
|
Log.error('Could not decode GIF for thumbnail.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final webp = await FlutterImageCompress.compressWithList(
|
||||||
|
pngBytes,
|
||||||
|
format: CompressFormat.webp,
|
||||||
|
quality: 85,
|
||||||
|
);
|
||||||
|
if (webp.isEmpty) {
|
||||||
|
Log.error('GIF thumbnail compression returned empty.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await destinationFile.writeAsBytes(webp);
|
||||||
|
|
||||||
|
stopwatch.stop();
|
||||||
|
if (destinationFile.existsSync() && destinationFile.lengthSync() > 0) {
|
||||||
|
Log.info(
|
||||||
|
'It took ${stopwatch.elapsedMilliseconds}ms to create the GIF thumbnail.',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error creating GIF thumbnail: $e');
|
||||||
|
try {
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
destinationFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List? _processGifThumbnail(Uint8List bytes) {
|
||||||
|
final image = img.decodeGif(bytes);
|
||||||
|
if (image == null) return null;
|
||||||
|
|
||||||
final thumbnail = img.copyResize(
|
final thumbnail = img.copyResize(
|
||||||
image,
|
image,
|
||||||
width: image.width > image.height ? 400 : null,
|
width: image.width > image.height ? 400 : null,
|
||||||
height: image.height >= image.width ? 400 : null,
|
height: image.height >= image.width ? 400 : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final pngBytes = img.encodePng(thumbnail);
|
return img.encodePng(thumbnail);
|
||||||
final webp = await FlutterImageCompress.compressWithList(
|
|
||||||
pngBytes,
|
|
||||||
format: CompressFormat.webp,
|
|
||||||
quality: 85,
|
|
||||||
);
|
|
||||||
destinationFile.writeAsBytesSync(webp);
|
|
||||||
|
|
||||||
stopwatch.stop();
|
|
||||||
Log.info(
|
|
||||||
'It took ${stopwatch.elapsedMilliseconds}ms to create the GIF thumbnail.',
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Error creating GIF thumbnail: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,12 @@ class MemoriesService {
|
||||||
|
|
||||||
Future<void> _initAsync() async {
|
Future<void> _initAsync() async {
|
||||||
try {
|
try {
|
||||||
|
// Start DB subscription first so files with existing thumbnails are shown immediately.
|
||||||
|
await _dbSubscription?.cancel();
|
||||||
|
_dbSubscription = twonlyDB.mediaFilesDao
|
||||||
|
.watchAllStoredMediaFiles()
|
||||||
|
.listen(_processMediaFilesStream);
|
||||||
|
|
||||||
final pendingFiles = await twonlyDB.mediaFilesDao
|
final pendingFiles = await twonlyDB.mediaFilesDao
|
||||||
.getAllMediaFilesPendingMigration();
|
.getAllMediaFilesPendingMigration();
|
||||||
|
|
||||||
|
|
@ -210,7 +216,57 @@ class MemoriesService {
|
||||||
);
|
);
|
||||||
_notifyState();
|
_notifyState();
|
||||||
|
|
||||||
|
// Run the multi-step background migration process asynchronously.
|
||||||
|
unawaited(_processMigrationQueue(pendingFiles));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error initializing MemoriesService: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _processMigrationQueue(List<MediaFile> pendingFiles) async {
|
||||||
|
try {
|
||||||
|
// Phase 1: Create thumbnails first so files can be shown in the
|
||||||
|
// gallery immediately, without waiting for heavier operations.
|
||||||
for (final mediaFile in pendingFiles) {
|
for (final mediaFile in pendingFiles) {
|
||||||
|
try {
|
||||||
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
|
||||||
|
if (!mediaService.mediaFile.hasThumbnail) {
|
||||||
|
if (mediaService.thumbnailPath.existsSync() &&
|
||||||
|
mediaService.thumbnailPath.lengthSync() > 0) {
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasThumbnail: Value(true)),
|
||||||
|
);
|
||||||
|
} else if (mediaFile.type != MediaType.audio) {
|
||||||
|
await mediaService.createThumbnail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(
|
||||||
|
'Error creating thumbnail for ${mediaFile.mediaId}: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_updateMigrationCount(_currentState.filesToMigrate - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateMigrationCount(0);
|
||||||
|
|
||||||
|
// Phase 2: Background — hash, crop analysis, size calculation.
|
||||||
|
// Each DB write here fires the stream subscription above, keeping
|
||||||
|
// the gallery state fresh without a separate notification step.
|
||||||
|
await _backgroundProcessPendingFiles(pendingFiles);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error in background migration queue: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _backgroundProcessPendingFiles(
|
||||||
|
List<MediaFile> pendingFiles,
|
||||||
|
) async {
|
||||||
|
for (final mediaFile in pendingFiles) {
|
||||||
|
try {
|
||||||
final mediaService = MediaFileService(mediaFile);
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
|
||||||
if (mediaService.mediaFile.storedFileHash == null) {
|
if (mediaService.mediaFile.storedFileHash == null) {
|
||||||
|
|
@ -224,29 +280,11 @@ class MemoriesService {
|
||||||
if (mediaService.mediaFile.sizeInBytes == null) {
|
if (mediaService.mediaFile.sizeInBytes == null) {
|
||||||
await mediaService.calculateAndSaveSize();
|
await mediaService.calculateAndSaveSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaService.mediaFile.hasThumbnail) {
|
|
||||||
if (mediaService.thumbnailPath.existsSync()) {
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
mediaFile.mediaId,
|
|
||||||
const MediaFilesCompanion(hasThumbnail: Value(true)),
|
|
||||||
);
|
|
||||||
} else if (mediaFile.type != MediaType.audio) {
|
|
||||||
await mediaService.createThumbnail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_updateMigrationCount(_currentState.filesToMigrate - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateMigrationCount(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _dbSubscription?.cancel();
|
|
||||||
_dbSubscription = twonlyDB.mediaFilesDao
|
|
||||||
.watchAllStoredMediaFiles()
|
|
||||||
.listen(_processMediaFilesStream);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Error initializing MemoriesService: $e');
|
Log.error(
|
||||||
|
'Error in background processing of ${mediaFile.mediaId}: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/json/signal_identity.model.dart';
|
import 'package:twonly/src/model/json/signal_identity.model.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
@ -144,6 +145,19 @@ Future<void> runMigrations() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userService.currentUser.appVersion < 116) {
|
||||||
|
if (userService.currentUser.userDiscoveryThreshold == 2) {
|
||||||
|
if (userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
|
await UserDiscoveryService.initializeOrUpdate(
|
||||||
|
threshold: 3,
|
||||||
|
sharePromotion: userService.currentUser.userDiscoverySharePromotion,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await UserService.update((u) => u..userDiscoveryThreshold = 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await UserService.update((u) => u.appVersion = 116);
|
||||||
|
}
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
assert(
|
assert(
|
||||||
AppState.latestAppVersionId == 116,
|
AppState.latestAppVersionId == 116,
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import 'dart:io' show Platform;
|
||||||
import 'package:firebase_app_installations/firebase_app_installations.dart';
|
import 'package:firebase_app_installations/firebase_app_installations.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
|
|
@ -107,6 +109,7 @@ Future<void> initFCMService() async {
|
||||||
);
|
);
|
||||||
|
|
||||||
unawaited(checkForTokenUpdates());
|
unawaited(checkForTokenUpdates());
|
||||||
|
unawaited(checkFcmHealthAndResetIfNeeded());
|
||||||
|
|
||||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
|
|
@ -133,6 +136,7 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleRemoteMessage(RemoteMessage message) async {
|
Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||||
|
await updateLastFcmMessageTimestamp();
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
Log.error('Got message in Dart while on iOS');
|
Log.error('Got message in Dart while on iOS');
|
||||||
}
|
}
|
||||||
|
|
@ -157,3 +161,100 @@ Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||||
// await handlePushData(message.data['push_data'] as String);
|
// await handlePushData(message.data['push_data'] as String);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateLastFcmMessageTimestamp() async {
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
try {
|
||||||
|
await storage.write(
|
||||||
|
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||||
|
value: nowMs,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Log.info('Updated last FCM message timestamp to $nowMs');
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not write last FCM message timestamp: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateLastServerMessageTimestamp() async {
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
try {
|
||||||
|
await storage.write(
|
||||||
|
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||||
|
value: nowMs,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Log.info('Updated last server message timestamp to $nowMs');
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not write last server message timestamp: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> checkFcmHealthAndResetIfNeeded() async {
|
||||||
|
if (!userService.isUserCreated) return;
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
try {
|
||||||
|
final lastFcmStr = await storage.read(
|
||||||
|
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final lastServerStr = await storage.read(
|
||||||
|
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final threeDaysAgo = now.subtract(const Duration(days: 3));
|
||||||
|
|
||||||
|
DateTime? lastFcmTime;
|
||||||
|
if (lastFcmStr != null) {
|
||||||
|
final ms = int.tryParse(lastFcmStr);
|
||||||
|
if (ms != null) {
|
||||||
|
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFcmTime != null) {
|
||||||
|
Log.info('Last message received via FCM messaging system: $lastFcmTime');
|
||||||
|
} else {
|
||||||
|
Log.info('No record of a message received via FCM messaging system.');
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? lastServerTime;
|
||||||
|
if (lastServerStr != null) {
|
||||||
|
final ms = int.tryParse(lastServerStr);
|
||||||
|
if (ms != null) {
|
||||||
|
lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check conditions:
|
||||||
|
// 1. No messages received via FCM in the last 3 days (either null or older than 3 days)
|
||||||
|
final fcmInactive = lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
|
||||||
|
// 2. Server message received within the last 3 days
|
||||||
|
final serverActive = lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
|
||||||
|
|
||||||
|
if (fcmInactive && serverActive) {
|
||||||
|
Log.warn('FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...');
|
||||||
|
await resetFCMTokens();
|
||||||
|
} else {
|
||||||
|
Log.info('FCM check passed. No reset needed.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error during FCM health check: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ class UserDiscoveryService {
|
||||||
final publicKey = await getUserPublicKey();
|
final publicKey = await getUserPublicKey();
|
||||||
Log.info('UserDiscoveryService: initializing Rust bridge');
|
Log.info('UserDiscoveryService: initializing Rust bridge');
|
||||||
await FlutterUserDiscovery.initializeOrUpdate(
|
await FlutterUserDiscovery.initializeOrUpdate(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
threshold: threshold,
|
threshold: threshold,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
|
|
@ -104,7 +105,7 @@ class UserDiscoveryService {
|
||||||
|
|
||||||
static Future<Uint8List?> getCurrentVersion() async {
|
static Future<Uint8List?> getCurrentVersion() async {
|
||||||
try {
|
try {
|
||||||
return await FlutterUserDiscovery.getCurrentVersion()
|
return await FlutterUserDiscovery.getCurrentVersion(callbackId: isolateCallbackId)
|
||||||
.timeout(const Duration(seconds: 5));
|
.timeout(const Duration(seconds: 5));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
|
|
@ -139,6 +140,7 @@ class UserDiscoveryService {
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
return await FlutterUserDiscovery.shouldRequestNewMessages(
|
return await FlutterUserDiscovery.shouldRequestNewMessages(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
contactId: fromUserId,
|
contactId: fromUserId,
|
||||||
version: receivedVersion,
|
version: receivedVersion,
|
||||||
).timeout(const Duration(seconds: 5));
|
).timeout(const Duration(seconds: 5));
|
||||||
|
|
@ -154,6 +156,7 @@ class UserDiscoveryService {
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
return await FlutterUserDiscovery.getNewMessages(
|
return await FlutterUserDiscovery.getNewMessages(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
contactId: fromUserId,
|
contactId: fromUserId,
|
||||||
receivedVersion: receivedVersion,
|
receivedVersion: receivedVersion,
|
||||||
).timeout(const Duration(seconds: 5));
|
).timeout(const Duration(seconds: 5));
|
||||||
|
|
@ -172,6 +175,7 @@ class UserDiscoveryService {
|
||||||
.getContactVerification(fromUserId);
|
.getContactVerification(fromUserId);
|
||||||
|
|
||||||
return await FlutterUserDiscovery.handleNewMessages(
|
return await FlutterUserDiscovery.handleNewMessages(
|
||||||
|
callbackId: isolateCallbackId,
|
||||||
contactId: fromUserId,
|
contactId: fromUserId,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
publicKeyVerifiedTimestamp:
|
publicKeyVerifiedTimestamp:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/physics.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
|
|
@ -18,8 +19,62 @@ class ContextMenu extends StatefulWidget {
|
||||||
State<ContextMenu> createState() => _ContextMenuState();
|
State<ContextMenu> createState() => _ContextMenuState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContextMenuState extends State<ContextMenu> {
|
class _ContextMenuState extends State<ContextMenu>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
Offset? _tapPosition;
|
Offset? _tapPosition;
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller =
|
||||||
|
AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
lowerBound: double.negativeInfinity,
|
||||||
|
upperBound: double.infinity,
|
||||||
|
value: 0,
|
||||||
|
)..addListener(() {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapDown(TapDownDetails details) {
|
||||||
|
_tapPosition = details.globalPosition;
|
||||||
|
_controller.animateTo(
|
||||||
|
1,
|
||||||
|
duration: const Duration(milliseconds: 60),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUp(TapUpDetails details) {
|
||||||
|
_bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapCancel() {
|
||||||
|
_bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _bounce() {
|
||||||
|
const spring = SpringDescription(
|
||||||
|
mass: 1,
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 15,
|
||||||
|
);
|
||||||
|
final simulation = SpringSimulation(
|
||||||
|
spring,
|
||||||
|
_controller.value,
|
||||||
|
0,
|
||||||
|
_controller.velocity,
|
||||||
|
);
|
||||||
|
_controller.animateWith(simulation);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _getIcon(dynamic icon) {
|
Widget _getIcon(dynamic icon) {
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -45,6 +100,7 @@ class _ContextMenuState extends State<ContextMenu> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unawaited(HapticFeedback.heavyImpact());
|
unawaited(HapticFeedback.heavyImpact());
|
||||||
|
_bounce();
|
||||||
|
|
||||||
await showMenu(
|
await showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -82,12 +138,16 @@ class _ContextMenuState extends State<ContextMenu> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final scale = 1.0 - (_controller.value * 0.02);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onLongPress: _showCustomMenu,
|
onLongPress: _showCustomMenu,
|
||||||
onTapDown: (details) {
|
onTapDown: _onTapDown,
|
||||||
_tapPosition = details.globalPosition;
|
onTapUp: _onTapUp,
|
||||||
},
|
onTapCancel: _onTapCancel,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: scale,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
243
lib/src/visual/elements/my_button.element.dart
Normal file
243
lib/src/visual/elements/my_button.element.dart
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/physics.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
enum MyButtonVariant {
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
text,
|
||||||
|
primaryMiddle,
|
||||||
|
primaryDense,
|
||||||
|
secondaryDense,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyButton extends StatefulWidget {
|
||||||
|
const MyButton({
|
||||||
|
required this.child,
|
||||||
|
required this.onPressed,
|
||||||
|
this.onLongPress,
|
||||||
|
this.variant = MyButtonVariant.primary,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
final MyButtonVariant variant;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyButton> createState() => _MyButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyButtonState extends State<MyButton>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller =
|
||||||
|
AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
lowerBound: double.negativeInfinity,
|
||||||
|
upperBound: double.infinity,
|
||||||
|
value: 0,
|
||||||
|
)..addListener(() {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapDown(TapDownDetails details) {
|
||||||
|
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||||
|
_controller.animateTo(
|
||||||
|
1,
|
||||||
|
duration: const Duration(milliseconds: 60),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUp(TapUpDetails details) {
|
||||||
|
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||||
|
_bounce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapCancel() {
|
||||||
|
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||||
|
_bounce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _bounce() {
|
||||||
|
const spring = SpringDescription(
|
||||||
|
mass: 1,
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 15,
|
||||||
|
);
|
||||||
|
final simulation = SpringSimulation(
|
||||||
|
spring,
|
||||||
|
_controller.value,
|
||||||
|
0,
|
||||||
|
_controller.velocity,
|
||||||
|
);
|
||||||
|
_controller.animateWith(simulation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// 0 (unpressed) -> scale 1.0
|
||||||
|
// 1 (pressed) -> scale 0.98 (subtle bounce)
|
||||||
|
final scale = 1.0 - (_controller.value * 0.02);
|
||||||
|
final isEnabled = widget.onPressed != null || widget.onLongPress != null;
|
||||||
|
final isDark = isDarkMode(context);
|
||||||
|
final disabledBgColor = isDark
|
||||||
|
? const Color(0xFF353535)
|
||||||
|
: const Color(0xFFE0E0E0);
|
||||||
|
final disabledFgColor = isDark
|
||||||
|
? const Color(0xFF757575)
|
||||||
|
: const Color(0xFF9E9E9E);
|
||||||
|
|
||||||
|
late final ButtonStyle buttonStyle;
|
||||||
|
switch (widget.variant) {
|
||||||
|
case MyButtonVariant.primary:
|
||||||
|
buttonStyle = FilledButton.styleFrom(
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
disabledBackgroundColor: disabledBgColor,
|
||||||
|
disabledForegroundColor: disabledFgColor,
|
||||||
|
minimumSize: const Size.fromHeight(60),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case MyButtonVariant.secondary:
|
||||||
|
buttonStyle = FilledButton.styleFrom(
|
||||||
|
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
|
||||||
|
foregroundColor: isDark ? Colors.white : Colors.black87,
|
||||||
|
disabledBackgroundColor: disabledBgColor,
|
||||||
|
disabledForegroundColor: disabledFgColor,
|
||||||
|
minimumSize: const Size.fromHeight(60),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case MyButtonVariant.text:
|
||||||
|
buttonStyle = TextButton.styleFrom(
|
||||||
|
minimumSize: const Size(0, 50),
|
||||||
|
foregroundColor: isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.7)
|
||||||
|
: Colors.black.withValues(alpha: 0.7),
|
||||||
|
disabledForegroundColor: disabledFgColor,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case MyButtonVariant.primaryMiddle:
|
||||||
|
buttonStyle = FilledButton.styleFrom(
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
disabledBackgroundColor: disabledBgColor,
|
||||||
|
disabledForegroundColor: disabledFgColor,
|
||||||
|
minimumSize: const Size(0, 48),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case MyButtonVariant.primaryDense:
|
||||||
|
buttonStyle = FilledButton.styleFrom(
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
disabledBackgroundColor: disabledBgColor,
|
||||||
|
disabledForegroundColor: disabledFgColor,
|
||||||
|
minimumSize: const Size(0, 40),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case MyButtonVariant.secondaryDense:
|
||||||
|
buttonStyle = FilledButton.styleFrom(
|
||||||
|
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
|
||||||
|
foregroundColor: isDark ? Colors.white : Colors.black87,
|
||||||
|
disabledBackgroundColor: disabledBgColor,
|
||||||
|
disabledForegroundColor: disabledFgColor,
|
||||||
|
minimumSize: const Size(0, 40),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final childButton = widget.variant == MyButtonVariant.text
|
||||||
|
? TextButton(
|
||||||
|
style: buttonStyle,
|
||||||
|
onPressed: isEnabled ? () {} : null,
|
||||||
|
child: widget.child,
|
||||||
|
)
|
||||||
|
: FilledButton(
|
||||||
|
style: buttonStyle,
|
||||||
|
onPressed: isEnabled ? () {} : null,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTapDown: isEnabled ? _onTapDown : null,
|
||||||
|
onTapUp: isEnabled ? _onTapUp : null,
|
||||||
|
onTapCancel: isEnabled ? _onTapCancel : null,
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
onLongPress: widget.onLongPress,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: scale,
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: childButton,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
237
lib/src/visual/elements/my_input.element.dart
Normal file
237
lib/src/visual/elements/my_input.element.dart
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/physics.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
class MyInput extends StatefulWidget {
|
||||||
|
const MyInput({
|
||||||
|
required this.controller,
|
||||||
|
this.onChanged,
|
||||||
|
this.onSubmitted,
|
||||||
|
this.inputFormatters,
|
||||||
|
this.hintText,
|
||||||
|
this.prefixIcon,
|
||||||
|
this.suffixIcon,
|
||||||
|
this.keyboardType,
|
||||||
|
this.autofocus = false,
|
||||||
|
this.errorText,
|
||||||
|
this.obscureText = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController controller;
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
|
final ValueChanged<String>? onSubmitted;
|
||||||
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
final String? hintText;
|
||||||
|
final Widget? prefixIcon;
|
||||||
|
final Widget? suffixIcon;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
final bool autofocus;
|
||||||
|
final String? errorText;
|
||||||
|
final bool obscureText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyInput> createState() => _MyInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyInputState extends State<MyInput> with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller =
|
||||||
|
AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
lowerBound: double.negativeInfinity,
|
||||||
|
upperBound: double.infinity,
|
||||||
|
value: 0,
|
||||||
|
)..addListener(() {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapDown(TapDownDetails details) {
|
||||||
|
_controller.animateTo(
|
||||||
|
1,
|
||||||
|
duration: const Duration(milliseconds: 60),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUp(TapUpDetails details) {
|
||||||
|
_bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapCancel() {
|
||||||
|
_bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _bounce() {
|
||||||
|
const spring = SpringDescription(
|
||||||
|
mass: 1,
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 15,
|
||||||
|
);
|
||||||
|
final simulation = SpringSimulation(
|
||||||
|
spring,
|
||||||
|
_controller.value,
|
||||||
|
0,
|
||||||
|
_controller.velocity,
|
||||||
|
);
|
||||||
|
_controller.animateWith(simulation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// 0 (unpressed) -> scale 1.0
|
||||||
|
// 1 (pressed) -> scale 0.98 (subtle bounce)
|
||||||
|
final scale = 1.0 - (_controller.value * 0.02);
|
||||||
|
final isDark = isDarkMode(context);
|
||||||
|
|
||||||
|
final inputFillColor = isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.08)
|
||||||
|
: Colors.black.withValues(alpha: 0.05);
|
||||||
|
|
||||||
|
final inputBorderColor = isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.15)
|
||||||
|
: Colors.black.withValues(alpha: 0.15);
|
||||||
|
|
||||||
|
final inputHintColor = isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.5)
|
||||||
|
: Colors.black.withValues(alpha: 0.5);
|
||||||
|
|
||||||
|
final prefixIconColor = isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.6)
|
||||||
|
: Colors.black.withValues(alpha: 0.6);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTapDown: _onTapDown,
|
||||||
|
onTapUp: _onTapUp,
|
||||||
|
onTapCancel: _onTapCancel,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: scale,
|
||||||
|
child: TextField(
|
||||||
|
controller: widget.controller,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
onSubmitted: widget.onSubmitted,
|
||||||
|
onTapOutside: (event) {
|
||||||
|
final pointer = event.pointer;
|
||||||
|
final startPosition = event.position;
|
||||||
|
var moved = false;
|
||||||
|
|
||||||
|
void handlePointerEvent(PointerEvent routeEvent) {
|
||||||
|
if (routeEvent is PointerMoveEvent) {
|
||||||
|
if ((routeEvent.position - startPosition).distance > 10) {
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
} else if (routeEvent is PointerUpEvent) {
|
||||||
|
GestureBinding.instance.pointerRouter.removeRoute(
|
||||||
|
pointer,
|
||||||
|
handlePointerEvent,
|
||||||
|
);
|
||||||
|
if (!moved) {
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
}
|
||||||
|
} else if (routeEvent is PointerCancelEvent) {
|
||||||
|
GestureBinding.instance.pointerRouter.removeRoute(
|
||||||
|
pointer,
|
||||||
|
handlePointerEvent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GestureBinding.instance.pointerRouter.addRoute(
|
||||||
|
pointer,
|
||||||
|
handlePointerEvent,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
inputFormatters: widget.inputFormatters,
|
||||||
|
keyboardType: widget.keyboardType,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
obscureText: widget.obscureText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: widget.hintText,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: inputHintColor,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: inputFillColor,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 18,
|
||||||
|
horizontal: 24,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: inputBorderColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: inputBorderColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorStyle: const TextStyle(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
errorText: widget.errorText,
|
||||||
|
prefixIcon: widget.prefixIcon != null
|
||||||
|
? IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: prefixIconColor,
|
||||||
|
),
|
||||||
|
child: widget.prefixIcon!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
suffixIcon: widget.suffixIcon != null
|
||||||
|
? IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
child: widget.suffixIcon!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,10 +25,20 @@ class ScreenshotImageHelper {
|
||||||
return imageBytes;
|
return imageBytes;
|
||||||
}
|
}
|
||||||
if (imageBytesFuture != null) {
|
if (imageBytesFuture != null) {
|
||||||
return imageBytesFuture;
|
try {
|
||||||
|
return imageBytes = await imageBytesFuture;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not resolve imageBytesFuture: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
return file!.readAsBytes();
|
try {
|
||||||
|
return imageBytes = await file!.readAsBytes();
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not read bytes from file: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (image == null) return null;
|
if (image == null) return null;
|
||||||
final img = await image!.toByteData(format: io.ImageByteFormat.png);
|
final img = await image!.toByteData(format: io.ImageByteFormat.png);
|
||||||
|
|
@ -61,7 +71,8 @@ class ScreenshotController {
|
||||||
var tmpPixelRatio = pixelRatio;
|
var tmpPixelRatio = pixelRatio;
|
||||||
if (tmpPixelRatio == null) {
|
if (tmpPixelRatio == null) {
|
||||||
if (context != null && context.mounted) {
|
if (context != null && context.mounted) {
|
||||||
tmpPixelRatio = tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
|
tmpPixelRatio =
|
||||||
|
tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
|
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> requestMicrophonePermission() async {
|
Future<void> requestMicrophonePermission({int retryCount = 0}) async {
|
||||||
|
try {
|
||||||
final statuses = await [
|
final statuses = await [
|
||||||
Permission.microphone,
|
Permission.microphone,
|
||||||
].request();
|
].request();
|
||||||
|
|
@ -266,6 +267,20 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
// _hasAudioPermission
|
// _hasAudioPermission
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
if (e.message?.contains('already running') ?? false) {
|
||||||
|
if (retryCount < 5) {
|
||||||
|
Log.warn(
|
||||||
|
'Microphone permission request conflict, retrying in 300ms... (attempt ${retryCount + 1})',
|
||||||
|
);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
return requestMicrophonePermission(retryCount: retryCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.error('PlatformException in requestMicrophonePermission: $e');
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error in requestMicrophonePermission: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateScaleFactor(double newScale) async {
|
Future<void> updateScaleFactor(double newScale) async {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||||
|
|
||||||
class SaveToGalleryButton extends StatefulWidget {
|
class SaveToGalleryButton extends StatefulWidget {
|
||||||
|
|
@ -33,18 +34,11 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return OutlinedButton(
|
final isEnabled = !widget.isLoading && !_imageSaving;
|
||||||
style: OutlinedButton.styleFrom(
|
return MyButton(
|
||||||
iconColor: _imageSaved
|
variant: MyButtonVariant.secondaryDense,
|
||||||
? Theme.of(context).colorScheme.outline
|
onPressed: isEnabled
|
||||||
: Theme.of(context).colorScheme.primary,
|
? () async {
|
||||||
foregroundColor: _imageSaved
|
|
||||||
? Theme.of(context).colorScheme.outline
|
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
onPressed: (widget.isLoading)
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_imageSaving = true;
|
_imageSaving = true;
|
||||||
});
|
});
|
||||||
|
|
@ -83,19 +77,24 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
_imageSaving = false;
|
_imageSaving = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (_imageSaving || widget.isLoading)
|
if (_imageSaving || widget.isLoading)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 1,
|
||||||
|
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
_imageSaved
|
_imageSaved
|
||||||
? const Icon(Icons.check)
|
? const Icon(Icons.check, size: 14)
|
||||||
: const FaIcon(FontAwesomeIcons.floppyDisk),
|
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 14),
|
||||||
if (widget.displayButtonLabel) const SizedBox(width: 10),
|
if (widget.displayButtonLabel) const SizedBox(width: 10),
|
||||||
if (widget.displayButtonLabel)
|
if (widget.displayButtonLabel)
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import 'package:twonly/src/visual/components/contact_request_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
||||||
import 'package:twonly/src/visual/elements/headline.element.dart';
|
import 'package:twonly/src/visual/elements/headline.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
|
||||||
|
|
@ -111,7 +112,9 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
|
|
||||||
for (final group in groups) {
|
for (final group in groups) {
|
||||||
if (group.pinned) continue;
|
if (group.pinned) continue;
|
||||||
if (!group.archived && getFlameCounterFromGroup(group).counter > 0 && bestFriends.length < 6) {
|
if (!group.archived &&
|
||||||
|
getFlameCounterFromGroup(group).counter > 0 &&
|
||||||
|
bestFriends.length < 6) {
|
||||||
bestFriends.add(group);
|
bestFriends.add(group);
|
||||||
} else {
|
} else {
|
||||||
otherUsers.add(group);
|
otherUsers.add(group);
|
||||||
|
|
@ -131,7 +134,10 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
await updateGroups(
|
await updateGroups(
|
||||||
_allGroups
|
_allGroups
|
||||||
.where(
|
.where(
|
||||||
(x) => !x.archived || !hideArchivedUsers || widget.selectedGroupIds.contains(x.groupId),
|
(x) =>
|
||||||
|
!x.archived ||
|
||||||
|
!hideArchivedUsers ||
|
||||||
|
widget.selectedGroupIds.contains(x.groupId),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
@ -193,7 +199,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
title: context.lang.shareImagePinnedContacts,
|
title: context.lang.shareImagePinnedContacts,
|
||||||
showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
|
showSelectAll:
|
||||||
|
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
BestFriendsSelector(
|
BestFriendsSelector(
|
||||||
|
|
@ -201,7 +208,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
title: context.lang.shareImageBestFriends,
|
title: context.lang.shareImageBestFriends,
|
||||||
showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
|
showSelectAll:
|
||||||
|
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
if (_otherUsers.isNotEmpty)
|
if (_otherUsers.isNotEmpty)
|
||||||
|
|
@ -264,7 +272,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
|
if (widget.mediaFileService.mediaFile.type ==
|
||||||
|
MediaType.image &&
|
||||||
_screenshotImage?.image != null &&
|
_screenshotImage?.image != null &&
|
||||||
userService.currentUser.showShowImagePreviewWhenSending)
|
userService.currentUser.showShowImagePreviewWhenSending)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
@ -288,22 +297,14 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
FilledButton.icon(
|
MyButton(
|
||||||
icon: !mediaStoreFutureReady || sendingImage
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
? SizedBox(
|
onPressed:
|
||||||
height: 12,
|
!mediaStoreFutureReady ||
|
||||||
width: 12,
|
widget.selectedGroupIds.isEmpty ||
|
||||||
child: CircularProgressIndicator.adaptive(
|
sendingImage
|
||||||
strokeWidth: 2,
|
? null
|
||||||
valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.inversePrimary),
|
: () async {
|
||||||
),
|
|
||||||
)
|
|
||||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
|
||||||
onPressed: () async {
|
|
||||||
if (!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
sendingImage = true;
|
sendingImage = true;
|
||||||
});
|
});
|
||||||
|
|
@ -319,19 +320,30 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
child: Row(
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
mainAxisSize: MainAxisSize.min,
|
||||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
children: [
|
||||||
),
|
if (!mediaStoreFutureReady || sendingImage)
|
||||||
backgroundColor: WidgetStateProperty.all<Color>(
|
const SizedBox(
|
||||||
!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
|
height: 14,
|
||||||
? context.color.onSurface
|
width: 14,
|
||||||
: context.color.primary,
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation(
|
||||||
|
Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: Text(
|
)
|
||||||
|
else
|
||||||
|
const FaIcon(
|
||||||
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
|
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
|
||||||
style: const TextStyle(fontSize: 17),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -18,6 +19,7 @@ import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
|
import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
|
||||||
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
|
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
|
||||||
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
|
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||||
|
|
@ -214,7 +216,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
|
|
||||||
List<Widget> get actionsAtTheRight {
|
List<Widget> get actionsAtTheRight {
|
||||||
if (layers.isNotEmpty &&
|
if (layers.isNotEmpty &&
|
||||||
(layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
(layers.first.isEditing ||
|
||||||
|
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
|
|
@ -290,9 +293,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
if (media.type == MediaType.video) ...[
|
if (media.type == MediaType.video) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ActionButton(
|
ActionButton(
|
||||||
(mediaService.removeAudio) ? Icons.volume_off_rounded : Icons.volume_up_rounded,
|
(mediaService.removeAudio)
|
||||||
|
? Icons.volume_off_rounded
|
||||||
|
: Icons.volume_up_rounded,
|
||||||
tooltipText: 'Enable Audio in Video',
|
tooltipText: 'Enable Audio in Video',
|
||||||
color: (mediaService.removeAudio) ? Colors.white.withAlpha(160) : Colors.white,
|
color: (mediaService.removeAudio)
|
||||||
|
? Colors.white.withAlpha(160)
|
||||||
|
: Colors.white,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await mediaService.toggleRemoveAudio();
|
await mediaService.toggleRemoveAudio();
|
||||||
if (mediaService.removeAudio) {
|
if (mediaService.removeAudio) {
|
||||||
|
|
@ -330,7 +337,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
FontAwesomeIcons.shieldHeart,
|
FontAwesomeIcons.shieldHeart,
|
||||||
tooltipText: context.lang.protectAsARealTwonly,
|
tooltipText: context.lang.protectAsARealTwonly,
|
||||||
color: media.requiresAuthentication ? Theme.of(context).colorScheme.primary : Colors.white,
|
color: media.requiresAuthentication
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.white,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await mediaService.setRequiresAuth(!media.requiresAuthentication);
|
await mediaService.setRequiresAuth(!media.requiresAuthentication);
|
||||||
selectedGroupIds = HashSet();
|
selectedGroupIds = HashSet();
|
||||||
|
|
@ -376,7 +385,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
|
|
||||||
List<Widget> get actionsAtTheTop {
|
List<Widget> get actionsAtTheTop {
|
||||||
if (layers.isNotEmpty &&
|
if (layers.isNotEmpty &&
|
||||||
(layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
(layers.first.isEditing ||
|
||||||
|
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
|
@ -468,7 +478,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
}
|
}
|
||||||
if (layers.length == 2) {
|
if (layers.length == 2) {
|
||||||
final filterLayer = layers[1];
|
final filterLayer = layers[1];
|
||||||
if (layers.first is BackgroundLayerData && filterLayer is FilterLayerData) {
|
if (layers.first is BackgroundLayerData &&
|
||||||
|
filterLayer is FilterLayerData) {
|
||||||
if (filterLayer.page == 1) {
|
if (filterLayer.page == 1) {
|
||||||
return (layers.first as BackgroundLayerData).image.image;
|
return (layers.first as BackgroundLayerData).image.image;
|
||||||
}
|
}
|
||||||
|
|
@ -501,6 +512,17 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ScreenshotImageHelper?> storeImageAsOriginal() async {
|
Future<ScreenshotImageHelper?> storeImageAsOriginal() async {
|
||||||
|
Uint8List? gifBytes;
|
||||||
|
ScreenshotImageHelper? image;
|
||||||
|
if (media.type == MediaType.gif) {
|
||||||
|
gifBytes = await widget.screenshotImage?.getBytes();
|
||||||
|
} else {
|
||||||
|
image = await getEditedImageBytes();
|
||||||
|
if (image != null) {
|
||||||
|
await image.getBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaService.overlayImagePath.existsSync()) {
|
if (mediaService.overlayImagePath.existsSync()) {
|
||||||
mediaService.overlayImagePath.deleteSync();
|
mediaService.overlayImagePath.deleteSync();
|
||||||
}
|
}
|
||||||
|
|
@ -512,14 +534,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
mediaService.originalPath.deleteSync();
|
mediaService.originalPath.deleteSync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScreenshotImageHelper? image;
|
|
||||||
if (media.type == MediaType.gif) {
|
if (media.type == MediaType.gif) {
|
||||||
final bytes = await widget.screenshotImage?.getBytes();
|
if (gifBytes != null) {
|
||||||
if (bytes != null) {
|
mediaService.originalPath.writeAsBytesSync(gifBytes.toList());
|
||||||
mediaService.originalPath.writeAsBytesSync(bytes.toList());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
image = await getEditedImageBytes();
|
|
||||||
if (image == null) return null;
|
if (image == null) return null;
|
||||||
final bytes = await image.getBytes();
|
final bytes = await image.getBytes();
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
|
|
@ -657,7 +677,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
await askToCloseThenClose();
|
await askToCloseThenClose();
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: widget.sharedFromGallery ? null : Colors.white.withAlpha(0),
|
backgroundColor: widget.sharedFromGallery
|
||||||
|
? null
|
||||||
|
: Colors.white.withAlpha(0),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
|
|
@ -701,48 +723,56 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
),
|
),
|
||||||
if (widget.sendToGroup != null) const SizedBox(width: 10),
|
if (widget.sendToGroup != null) const SizedBox(width: 10),
|
||||||
if (widget.sendToGroup != null)
|
if (widget.sendToGroup != null)
|
||||||
OutlinedButton(
|
MyButton(
|
||||||
style: OutlinedButton.styleFrom(
|
variant: MyButtonVariant.secondaryDense,
|
||||||
iconColor: Theme.of(context).colorScheme.primary,
|
|
||||||
foregroundColor: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
),
|
|
||||||
onPressed: pushShareImageView,
|
onPressed: pushShareImageView,
|
||||||
child: const FaIcon(FontAwesomeIcons.userPlus),
|
child: const FaIcon(
|
||||||
|
FontAwesomeIcons.userPlus,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: widget.sendToGroup == null ? 20 : 10),
|
SizedBox(width: widget.sendToGroup == null ? 20 : 10),
|
||||||
FilledButton.icon(
|
IntrinsicWidth(
|
||||||
icon: sendingOrLoadingImage
|
child: MyButton(
|
||||||
? SizedBox(
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
height: 12,
|
onPressed: sendingOrLoadingImage
|
||||||
width: 12,
|
? null
|
||||||
child: CircularProgressIndicator.adaptive(
|
: () async {
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.inversePrimary),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
|
||||||
onPressed: () async {
|
|
||||||
if (sendingOrLoadingImage) return;
|
|
||||||
if (widget.sendToGroup == null) {
|
if (widget.sendToGroup == null) {
|
||||||
return pushShareImageView();
|
return pushShareImageView();
|
||||||
}
|
}
|
||||||
await sendImageToSinglePerson();
|
await sendImageToSinglePerson();
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
child: Row(
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
mainAxisSize: MainAxisSize.min,
|
||||||
const EdgeInsets.symmetric(
|
children: [
|
||||||
vertical: 10,
|
if (sendingOrLoadingImage)
|
||||||
horizontal: 30,
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
width: 12,
|
||||||
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation(
|
||||||
|
Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const FaIcon(
|
||||||
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
|
size: 14,
|
||||||
),
|
),
|
||||||
label: Text(
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
(widget.sendToGroup == null)
|
(widget.sendToGroup == null)
|
||||||
? context.lang.shareImagedEditorShareWith
|
? context.lang.shareImagedEditorShareWith
|
||||||
: substringBy(widget.sendToGroup!.groupName, 15),
|
: substringBy(
|
||||||
style: const TextStyle(fontSize: 17),
|
widget.sendToGroup!.groupName,
|
||||||
|
15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,10 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
|
|
||||||
bool _hasContacts = false;
|
bool _hasContacts = false;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
bool get _hasOpenGroup => _groupsNotPinned.isNotEmpty || _groupsArchived.isNotEmpty || _groupsPinned.isNotEmpty;
|
bool get _hasOpenGroup =>
|
||||||
|
_groupsNotPinned.isNotEmpty ||
|
||||||
|
_groupsArchived.isNotEmpty ||
|
||||||
|
_groupsPinned.isNotEmpty;
|
||||||
|
|
||||||
GlobalKey searchForOtherUsers = GlobalKey();
|
GlobalKey searchForOtherUsers = GlobalKey();
|
||||||
bool showFeedbackShortcut = false;
|
bool showFeedbackShortcut = false;
|
||||||
|
|
@ -64,21 +67,27 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
_contactsSub = stream.listen((groups) {
|
_contactsSub = stream.listen((groups) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_groupsNotPinned = groups.where((x) => !x.pinned && !x.archived).toList();
|
_groupsNotPinned = groups
|
||||||
|
.where((x) => !x.pinned && !x.archived)
|
||||||
|
.toList();
|
||||||
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
|
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
|
||||||
_groupsArchived = groups.where((x) => x.archived).toList();
|
_groupsArchived = groups.where((x) => x.archived).toList();
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_contactsCountSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((contacts) {
|
_contactsCountSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((
|
||||||
|
contacts,
|
||||||
|
) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_hasContacts = contacts.isNotEmpty;
|
_hasContacts = contacts.isNotEmpty;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_countContactRequestStream = twonlyDB.contactsDao.watchContactsRequestedCount().listen((update) {
|
_countContactRequestStream = twonlyDB.contactsDao
|
||||||
|
.watchContactsRequestedCount()
|
||||||
|
.listen((update) {
|
||||||
if (update != null) {
|
if (update != null) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -87,7 +96,9 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_countAnnouncedStream = twonlyDB.userDiscoveryDao.watchNewAnnouncementsWithDataCount().listen((update) {
|
_countAnnouncedStream = twonlyDB.userDiscoveryDao
|
||||||
|
.watchNewAnnouncementsWithDataCount()
|
||||||
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_countAnnouncedUsers = update;
|
_countAnnouncedUsers = update;
|
||||||
|
|
@ -101,7 +112,8 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
changeLog.codeUnits,
|
changeLog.codeUnits,
|
||||||
)).bytes;
|
)).bytes;
|
||||||
if (!userService.currentUser.hideChangeLog &&
|
if (!userService.currentUser.hideChangeLog &&
|
||||||
userService.currentUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
userService.currentUser.lastChangeLogHash.toString() !=
|
||||||
|
changeLogHash.toString()) {
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u.lastChangeLogHash = changeLogHash;
|
u.lastChangeLogHash = changeLogHash;
|
||||||
});
|
});
|
||||||
|
|
@ -190,11 +202,16 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: NotificationBadgeComp(
|
child: NotificationBadgeComp(
|
||||||
backgroundColor: isDarkMode(context) ? Colors.white : Colors.black,
|
backgroundColor: isDarkMode(context)
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
count: (_countAnnouncedUsers + _countContactRequest).toString(),
|
count: (_countAnnouncedUsers + _countContactRequest)
|
||||||
|
.toString(),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
color: (_countAnnouncedUsers + _countContactRequest > 0) ? Colors.black : null,
|
color: (_countAnnouncedUsers + _countContactRequest > 0)
|
||||||
|
? Colors.black
|
||||||
|
: null,
|
||||||
key: searchForOtherUsers,
|
key: searchForOtherUsers,
|
||||||
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
|
@ -240,7 +257,10 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
_groupsNotPinned.length +
|
_groupsNotPinned.length +
|
||||||
(_groupsArchived.isNotEmpty ? 1 : 0),
|
(_groupsArchived.isNotEmpty ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index >= _groupsNotPinned.length + _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0)) {
|
if (index >=
|
||||||
|
_groupsNotPinned.length +
|
||||||
|
_groupsPinned.length +
|
||||||
|
(_groupsPinned.isNotEmpty ? 1 : 0)) {
|
||||||
if (_groupsArchived.isEmpty) return Container();
|
if (_groupsArchived.isEmpty) return Container();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
@ -291,32 +311,33 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Material(
|
FloatingActionButton(
|
||||||
elevation: 3,
|
heroTag: 'qrcode_fab',
|
||||||
shape: const CircleBorder(),
|
elevation: 2,
|
||||||
color: context.color.primary,
|
backgroundColor: isDarkMode(context)
|
||||||
child: InkWell(
|
? Colors.grey[800]
|
||||||
borderRadius: BorderRadius.circular(12),
|
: Colors.grey[200],
|
||||||
onTap: () => context.push(Routes.settingsPublicProfile),
|
foregroundColor: isDarkMode(context)
|
||||||
child: SizedBox(
|
? Colors.white
|
||||||
width: 45,
|
: Colors.black87,
|
||||||
height: 45,
|
onPressed: () => context.push(Routes.settingsPublicProfile),
|
||||||
child: Center(
|
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.qrcode,
|
FontAwesomeIcons.qrcode,
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
color: isDarkMode(context)
|
||||||
),
|
? Colors.white
|
||||||
),
|
: Colors.black87,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
backgroundColor: context.color.primary,
|
heroTag: 'new_chat_fab',
|
||||||
|
elevation: 2,
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
onPressed: () => context.push(Routes.chatsStartNewChat),
|
onPressed: () => context.push(Routes.chatsStartNewChat),
|
||||||
child: FaIcon(
|
child: const FaIcon(
|
||||||
FontAwesomeIcons.penToSquare,
|
FontAwesomeIcons.penToSquare,
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart' show FaIcon, FontAwesomeIcons;
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart'
|
||||||
|
show FaIcon, FontAwesomeIcons;
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
show ProfileQrCodeComp;
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
class EmptyChatListComp extends StatelessWidget {
|
class EmptyChatListComp extends StatelessWidget {
|
||||||
const EmptyChatListComp({super.key});
|
const EmptyChatListComp({super.key});
|
||||||
|
|
@ -17,7 +19,8 @@ class EmptyChatListComp extends StatelessWidget {
|
||||||
try {
|
try {
|
||||||
final pubKey = await getUserPublicKey();
|
final pubKey = await getUserPublicKey();
|
||||||
final params = ShareParams(
|
final params = ShareParams(
|
||||||
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
text:
|
||||||
|
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||||
);
|
);
|
||||||
await SharePlus.instance.share(params);
|
await SharePlus.instance.share(params);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -37,16 +40,16 @@ class EmptyChatListComp extends StatelessWidget {
|
||||||
height: 24,
|
height: 24,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
),
|
),
|
||||||
const Text(
|
Text(
|
||||||
'Find your first friend',
|
context.lang.emptyChatListTitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Let friends scan your QR code, or share them your profile.',
|
context.lang.emptyChatListDesc,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: context.color.onSurface.withValues(alpha: 0.6),
|
color: context.color.onSurface.withValues(alpha: 0.6),
|
||||||
|
|
@ -56,61 +59,69 @@ class EmptyChatListComp extends StatelessWidget {
|
||||||
const SizedBox(height: 36),
|
const SizedBox(height: 36),
|
||||||
const Center(child: ProfileQrCodeComp()),
|
const Center(child: ProfileQrCodeComp()),
|
||||||
const SizedBox(height: 36),
|
const SizedBox(height: 36),
|
||||||
// 3. Action Buttons
|
IntrinsicWidth(
|
||||||
// Button 1: Share Profile (Full Width)
|
child: MyButton(
|
||||||
FilledButton.icon(
|
|
||||||
style: primaryColorButtonStyle,
|
|
||||||
onPressed: () => _shareProfile(context),
|
onPressed: () => _shareProfile(context),
|
||||||
icon: const FaIcon(FontAwesomeIcons.shareNodes, size: 20),
|
child: Row(
|
||||||
label: const Text(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
'Share your profile',
|
children: [
|
||||||
style: TextStyle(
|
const FaIcon(FontAwesomeIcons.shareNodes, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
context.lang.emptyChatListShareBtn,
|
||||||
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Button Row: Scan QR Code & Enter Username
|
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
MyButton(
|
||||||
child: FilledButton.icon(
|
variant: MyButtonVariant.secondaryDense,
|
||||||
style: secondaryGreyButtonStyle(context),
|
|
||||||
onPressed: () => context.push(Routes.cameraQRScanner),
|
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||||
icon: const Icon(Icons.qr_code_scanner_rounded, size: 20),
|
child: Row(
|
||||||
label: const FittedBox(
|
mainAxisSize: MainAxisSize.min,
|
||||||
fit: BoxFit.scaleDown,
|
children: [
|
||||||
child: Text(
|
const Icon(Icons.qr_code_scanner_rounded, size: 20),
|
||||||
'Scan QR Code',
|
const SizedBox(width: 8),
|
||||||
style: TextStyle(
|
Text(
|
||||||
|
context.lang.emptyChatListScanBtn,
|
||||||
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: FilledButton.icon(
|
|
||||||
style: secondaryGreyButtonStyle(context),
|
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
|
||||||
icon: const Icon(Icons.person_add_rounded, size: 20),
|
|
||||||
label: const FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text(
|
|
||||||
'Add by Username',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryDense,
|
||||||
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.person_add_rounded, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
context.lang.emptyChatListAddUsernameBtn,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 50),
|
const SizedBox(height: 50),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class _AnimatedNewMessageState extends State<AnimatedNewMessage>
|
||||||
parent: _controller,
|
parent: _controller,
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
),
|
),
|
||||||
axisAlignment: 1,
|
alignment: Alignment.bottomLeft,
|
||||||
child: ScaleTransition(
|
child: ScaleTransition(
|
||||||
scale: _scaleAnimation,
|
scale: _scaleAnimation,
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
|
|
|
||||||
|
|
@ -31,23 +31,13 @@ class ChatAudioEntry extends StatelessWidget {
|
||||||
return Container(); // media file was purged
|
return Container(); // media file was purged
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
final showTime = info.displayTime || message.modifiedAt != null;
|
||||||
builder: (context, constraints) {
|
|
||||||
final textWidth = measureTextWidth(info.text);
|
|
||||||
const timeWidth = 60.0;
|
|
||||||
final isExpanded =
|
|
||||||
info.expanded ||
|
|
||||||
(textWidth + timeWidth + 20 > constraints.maxWidth);
|
|
||||||
final effectiveSpacerWidth =
|
|
||||||
constraints.minWidth - textWidth - timeWidth;
|
|
||||||
final spacerWidth = effectiveSpacerWidth > 0
|
|
||||||
? effectiveSpacerWidth
|
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
return Container(
|
return IntrinsicWidth(
|
||||||
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
minWidth: 250,
|
minWidth: 280,
|
||||||
),
|
),
|
||||||
padding: info.padding,
|
padding: info.padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -56,9 +46,12 @@ class ChatAudioEntry extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (info.displayUserName != '')
|
if (info.displayUserName != '')
|
||||||
Text(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
info.displayUserName,
|
info.displayUserName,
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|
@ -66,42 +59,37 @@ class ChatAudioEntry extends StatelessWidget {
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (isExpanded && info.text != '')
|
if (info.text != '')
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BetterText(
|
child: BetterText(
|
||||||
text: info.text,
|
text: info.text,
|
||||||
textColor: info.textColor,
|
textColor: info.textColor,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (info.text != '') ...[
|
else
|
||||||
BetterText(text: info.text, textColor: info.textColor),
|
Expanded(
|
||||||
SizedBox(width: spacerWidth),
|
child: mediaService.mediaFile.downloadState ==
|
||||||
] else ...[
|
|
||||||
if (mediaService.mediaFile.downloadState ==
|
|
||||||
DownloadState.ready ||
|
DownloadState.ready ||
|
||||||
mediaService.mediaFile.downloadState == null)
|
mediaService.mediaFile.downloadState == null
|
||||||
mediaService.tempPath.existsSync()
|
? (mediaService.tempPath.existsSync()
|
||||||
? InChatAudioPlayer(
|
? InChatAudioPlayer(
|
||||||
path: mediaService.tempPath.path,
|
path: mediaService.tempPath.path,
|
||||||
message: message,
|
message: message,
|
||||||
)
|
)
|
||||||
: Container()
|
: Container())
|
||||||
else
|
: MessageSendStateIcon([message], [mediaService.mediaFile]),
|
||||||
MessageSendStateIcon([message], [mediaService.mediaFile]),
|
),
|
||||||
SizedBox(width: spacerWidth),
|
if (showTime) FriendlyMessageTime(message: message),
|
||||||
],
|
|
||||||
if (info.displayTime || message.modifiedAt != null)
|
|
||||||
FriendlyMessageTime(message: message),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,20 +34,10 @@ class ChatTextEntry extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
final showTime = info.displayTime || message.modifiedAt != null;
|
||||||
builder: (context, constraints) {
|
|
||||||
final textWidth = measureTextWidth(info.text);
|
|
||||||
const timeWidth = 60.0;
|
|
||||||
final isExpanded =
|
|
||||||
info.expanded ||
|
|
||||||
(textWidth + timeWidth + 20 > constraints.maxWidth);
|
|
||||||
final effectiveSpacerWidth =
|
|
||||||
constraints.minWidth - textWidth - timeWidth;
|
|
||||||
final spacerWidth = effectiveSpacerWidth > 0
|
|
||||||
? effectiveSpacerWidth
|
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
return Container(
|
return IntrinsicWidth(
|
||||||
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
minWidth: info.minWidth,
|
minWidth: info.minWidth,
|
||||||
|
|
@ -59,9 +49,12 @@ class ChatTextEntry extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (info.displayUserName != '')
|
if (info.displayUserName != '')
|
||||||
Text(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
info.displayUserName,
|
info.displayUserName,
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|
@ -69,31 +62,23 @@ class ChatTextEntry extends StatelessWidget {
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (isExpanded)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BetterText(
|
child: BetterText(
|
||||||
text: info.text,
|
text: info.text,
|
||||||
textColor: info.textColor,
|
textColor: info.textColor,
|
||||||
),
|
),
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
BetterText(text: info.text, textColor: info.textColor),
|
|
||||||
SizedBox(
|
|
||||||
width: spacerWidth,
|
|
||||||
),
|
),
|
||||||
],
|
if (showTime) FriendlyMessageTime(message: message),
|
||||||
if (info.displayTime || message.modifiedAt != null)
|
|
||||||
FriendlyMessageTime(message: message),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 1, valueColor: AlwaysStoppedAnimation(color)),
|
child: CircularProgressIndicator(strokeWidth: 1, color: color),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
|
||||||
|
|
||||||
class ResponseContainer extends StatefulWidget {
|
class ResponseContainer extends StatelessWidget {
|
||||||
const ResponseContainer({
|
const ResponseContainer({
|
||||||
required this.msg,
|
required this.msg,
|
||||||
required this.group,
|
required this.group,
|
||||||
|
|
@ -27,60 +27,34 @@ class ResponseContainer extends StatefulWidget {
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
final void Function(String)? scrollToMessage;
|
final void Function(String)? scrollToMessage;
|
||||||
|
|
||||||
@override
|
|
||||||
State<ResponseContainer> createState() => _ResponseContainerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ResponseContainerState extends State<ResponseContainer> {
|
|
||||||
double? minWidth;
|
|
||||||
final GlobalKey _message = GlobalKey();
|
|
||||||
final GlobalKey _preview = GlobalKey();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final messageBox = _message.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
final previewBox = _preview.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
if (messageBox == null || previewBox == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
if (messageBox.size.width > previewBox.size.width) {
|
|
||||||
minWidth = messageBox.size.width;
|
|
||||||
} else {
|
|
||||||
minWidth = previewBox.size.width;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.msg.quotesMessageId == null) {
|
if (msg.quotesMessageId == null) {
|
||||||
if (widget.child == null) {
|
if (child == null) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
return widget.child!;
|
return child!;
|
||||||
}
|
}
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: widget.scrollToMessage == null ? null : () => widget.scrollToMessage!(widget.msg.quotesMessageId!),
|
onTap: scrollToMessage == null
|
||||||
|
? null
|
||||||
|
: () => scrollToMessage!(msg.quotesMessageId!),
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: getMessageColor(widget.msg.senderId != null),
|
color: getMessageColor(msg.senderId != null),
|
||||||
borderRadius: widget.borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
|
child: IntrinsicWidth(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
key: _preview,
|
|
||||||
padding: const EdgeInsets.only(top: 4, right: 4, left: 4),
|
padding: const EdgeInsets.only(top: 4, right: 4, left: 4),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: minWidth,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.color.surface.withAlpha(150),
|
color: context.color.surface.withAlpha(150),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
|
|
@ -91,20 +65,19 @@ class _ResponseContainerState extends State<ResponseContainer> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ResponsePreview(
|
child: ResponsePreview(
|
||||||
group: widget.group,
|
group: group,
|
||||||
messageId: widget.msg.quotesMessageId,
|
messageId: msg.quotesMessageId,
|
||||||
showBorder: false,
|
showBorder: false,
|
||||||
|
showLeftBorder: false,
|
||||||
|
colorUsername: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
if (child != null) child!,
|
||||||
key: _message,
|
|
||||||
width: minWidth,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +88,8 @@ class ResponsePreview extends StatefulWidget {
|
||||||
required this.showBorder,
|
required this.showBorder,
|
||||||
this.message,
|
this.message,
|
||||||
this.messageId,
|
this.messageId,
|
||||||
|
this.showLeftBorder = true,
|
||||||
|
this.colorUsername = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -122,6 +97,8 @@ class ResponsePreview extends StatefulWidget {
|
||||||
final String? messageId;
|
final String? messageId;
|
||||||
final Group group;
|
final Group group;
|
||||||
final bool showBorder;
|
final bool showBorder;
|
||||||
|
final bool showLeftBorder;
|
||||||
|
final bool colorUsername;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ResponsePreview> createState() => _ResponsePreviewState();
|
State<ResponsePreview> createState() => _ResponsePreviewState();
|
||||||
|
|
@ -140,12 +117,16 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
_message ??= await twonlyDB.messagesDao.getMessageById(widget.messageId!).getSingleOrNull();
|
_message ??= await twonlyDB.messagesDao
|
||||||
|
.getMessageById(widget.messageId!)
|
||||||
|
.getSingleOrNull();
|
||||||
if (_message?.mediaId != null) {
|
if (_message?.mediaId != null) {
|
||||||
_mediaService = await MediaFileService.fromMediaId(_message!.mediaId!);
|
_mediaService = await MediaFileService.fromMediaId(_message!.mediaId!);
|
||||||
}
|
}
|
||||||
if (_message?.senderId != null) {
|
if (_message?.senderId != null) {
|
||||||
final contact = await twonlyDB.contactsDao.getContactByUserId(_message!.senderId!).getSingleOrNull();
|
final contact = await twonlyDB.contactsDao
|
||||||
|
.getContactByUserId(_message!.senderId!)
|
||||||
|
.getSingleOrNull();
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
_username = getContactDisplayName(contact);
|
_username = getContactDisplayName(contact);
|
||||||
}
|
}
|
||||||
|
|
@ -204,74 +185,85 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
||||||
|
|
||||||
if (_message!.senderId == null) {
|
if (_message!.senderId == null) {
|
||||||
_username = context.lang.you;
|
_username = context.lang.you;
|
||||||
// _username = _message!.senderId.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
color = getMessageColor(_message!.senderId != null);
|
color = getMessageColor(_message!.senderId != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasImage =
|
||||||
|
_message != null &&
|
||||||
|
_message!.mediaStored &&
|
||||||
|
_mediaService != null &&
|
||||||
|
_mediaService!.mediaFile.type != MediaType.audio;
|
||||||
|
|
||||||
|
Widget? imageWidget;
|
||||||
|
if (hasImage) {
|
||||||
|
final isVideo = _mediaService!.mediaFile.type == MediaType.video;
|
||||||
|
final pathToCheck = isVideo
|
||||||
|
? _mediaService!.thumbnailPath
|
||||||
|
: _mediaService!.storedPath;
|
||||||
|
if (pathToCheck.existsSync() && pathToCheck.lengthSync() > 0) {
|
||||||
|
imageWidget = Container(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
margin: const EdgeInsets.only(left: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Image.file(
|
||||||
|
pathToCheck,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!_message!.mediaStored) {
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: widget.showBorder
|
padding: EdgeInsets.only(
|
||||||
? const EdgeInsets.only(left: 10, right: 10)
|
left: widget.showLeftBorder ? 8 : 4,
|
||||||
: const EdgeInsets.symmetric(horizontal: 5),
|
right: 6,
|
||||||
decoration: (widget.showBorder)
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: 60,
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.7,
|
||||||
|
),
|
||||||
|
decoration: widget.showLeftBorder
|
||||||
? BoxDecoration(
|
? BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
left: BorderSide(
|
left: BorderSide(
|
||||||
color: color,
|
color: color,
|
||||||
width: 2,
|
width: 2.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_username,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
if (subtitle != null) Text(subtitle),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.only(left: 10),
|
|
||||||
width: 200,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
left: BorderSide(
|
|
||||||
color: color,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_username,
|
_username,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: widget.colorUsername ? color : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null)
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if (subtitle != null) Text(subtitle),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_mediaService != null && _mediaService!.mediaFile.type != MediaType.audio)
|
if (imageWidget != null) imageWidget,
|
||||||
SizedBox(
|
|
||||||
height: widget.showBorder ? 100 : 210,
|
|
||||||
child: Image.file(
|
|
||||||
_mediaService!.mediaFile.type == MediaType.video
|
|
||||||
? _mediaService!.thumbnailPath
|
|
||||||
: _mediaService!.storedPath,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
allMediaFiles.add(msg);
|
allMediaFiles.add(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (allMediaFiles.length > 1) {
|
||||||
|
if (widget.initialMessage == null &&
|
||||||
|
currentMedia == null &&
|
||||||
|
!_showDownloadingLoader) {
|
||||||
|
allMediaFiles.sort(
|
||||||
|
(a, b) => a.createdAt.compareTo(b.createdAt),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final upcoming = allMediaFiles.sublist(1)
|
||||||
|
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||||
|
allMediaFiles = [allMediaFiles.first, ...upcoming];
|
||||||
|
}
|
||||||
|
}
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
if (firstRun) {
|
if (firstRun) {
|
||||||
firstRun = false;
|
firstRun = false;
|
||||||
|
|
@ -698,6 +711,15 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
),
|
),
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.broken_image_outlined,
|
||||||
|
color: Colors.white38,
|
||||||
|
size: 64,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
context.lang.userFoundBody,
|
context.lang.userFoundBody(widget.profile.username),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: context.color.onSurfaceVariant,
|
color: context.color.onSurfaceVariant,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart';
|
import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart';
|
import 'package:twonly/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart';
|
||||||
|
|
||||||
|
|
@ -40,6 +41,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
final TextEditingController _usernameController = TextEditingController();
|
final TextEditingController _usernameController = TextEditingController();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool hasRequestedUsers = false;
|
bool hasRequestedUsers = false;
|
||||||
|
String? _searchError;
|
||||||
|
|
||||||
List<Contact> _openRequestsContacts = [];
|
List<Contact> _openRequestsContacts = [];
|
||||||
late StreamSubscription<List<Contact>> _contactsStream;
|
late StreamSubscription<List<Contact>> _contactsStream;
|
||||||
|
|
@ -63,14 +65,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchNewAnnouncedUsersWithRelations().listen((update) {
|
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
||||||
|
.watchNewAnnouncedUsersWithRelations()
|
||||||
|
.listen((update) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_newAnnouncedUsers = update;
|
_newAnnouncedUsers = update;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchAllAnnouncedUsersWithRelations().listen((update) {
|
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
||||||
|
.watchAllAnnouncedUsersWithRelations()
|
||||||
|
.listen((update) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_allAnnouncedUsers = update;
|
_allAnnouncedUsers = update;
|
||||||
|
|
@ -90,7 +96,8 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
Future<void> _shareProfile() async {
|
Future<void> _shareProfile() async {
|
||||||
final pubKey = await getUserPublicKey();
|
final pubKey = await getUserPublicKey();
|
||||||
final params = ShareParams(
|
final params = ShareParams(
|
||||||
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
text:
|
||||||
|
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||||
);
|
);
|
||||||
await SharePlus.instance.share(params);
|
await SharePlus.instance.share(params);
|
||||||
}
|
}
|
||||||
|
|
@ -164,18 +171,20 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userdata == null) {
|
if (userdata == null) {
|
||||||
await showAlertDialog(
|
setState(() {
|
||||||
context,
|
_searchError = context.lang.searchUsernameNotFound;
|
||||||
context.lang.searchUsernameNotFound,
|
});
|
||||||
context.lang.searchUsernameNotFoundBody(username),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_searchError = null;
|
||||||
|
});
|
||||||
|
|
||||||
final addUser = await showAlertDialog(
|
final addUser = await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
context.lang.userFound(username),
|
context.lang.userFound(username),
|
||||||
context.lang.userFoundBody,
|
context.lang.userFoundBody(username),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!addUser || !mounted) return;
|
if (!addUser || !mounted) return;
|
||||||
|
|
@ -190,7 +199,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.publicKey != null && mounted && widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
if (widget.publicKey != null &&
|
||||||
|
mounted &&
|
||||||
|
widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
||||||
final markAsVerified = await showAlertDialog(
|
final markAsVerified = await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
context.lang.linkFromUsername(username),
|
context.lang.linkFromUsername(username),
|
||||||
|
|
@ -218,137 +229,135 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||||
child: SearchBar(
|
child: MyInput(
|
||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
hintText: context.lang.searchUsernameInput,
|
hintText: context.lang.searchUsernameInput,
|
||||||
elevation: const WidgetStatePropertyAll(0),
|
prefixIcon: const Icon(Icons.search, size: 20),
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
errorText: _searchError,
|
||||||
context.color.surfaceContainerHighest.withValues(alpha: 0.3),
|
|
||||||
),
|
|
||||||
shape: WidgetStatePropertyAll(
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const WidgetStatePropertyAll(
|
|
||||||
EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
),
|
|
||||||
leading: const Icon(Icons.search, size: 20, color: Colors.grey),
|
|
||||||
trailing: [
|
|
||||||
if (_usernameController.text.isNotEmpty) ...[
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.clear, size: 20),
|
|
||||||
onPressed: () {
|
|
||||||
_usernameController.clear();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_isLoading)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(
|
|
||||||
FontAwesomeIcons.magnifyingGlassPlus,
|
|
||||||
size: 20,
|
|
||||||
color: context.color.primary,
|
|
||||||
),
|
|
||||||
onPressed: () => _requestNewUserByUsername(
|
|
||||||
_usernameController.text,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(
|
|
||||||
FontAwesomeIcons.camera,
|
|
||||||
size: 20,
|
|
||||||
color: context.color.primary,
|
|
||||||
),
|
|
||||||
onPressed: () => context.push(Routes.cameraQRScanner),
|
|
||||||
tooltip: context.lang.scanOtherProfile,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
onSubmitted: _requestNewUserByUsername,
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_usernameController.text = value.toLowerCase();
|
_usernameController.text = value.toLowerCase();
|
||||||
_usernameController.selection = TextSelection.fromPosition(
|
_usernameController.selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: _usernameController.text.length),
|
TextPosition(offset: _usernameController.text.length),
|
||||||
);
|
);
|
||||||
|
setState(() {
|
||||||
|
_searchError = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSubmitted: _requestNewUserByUsername,
|
||||||
|
suffixIcon: _usernameController.text.isNotEmpty
|
||||||
|
? Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton.filled(
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
context.color.surfaceContainerHighest,
|
||||||
|
foregroundColor: context.color.onSurface,
|
||||||
|
minimumSize: const Size(32, 32),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
),
|
||||||
|
iconSize: 16,
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
_usernameController.clear();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 0),
|
||||||
|
if (_isLoading)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
context.color.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton.filled(
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
foregroundColor: context.color.onPrimary,
|
||||||
|
minimumSize: const Size(32, 32),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
),
|
||||||
|
iconSize: 16,
|
||||||
|
icon: const Icon(Icons.person_add_rounded),
|
||||||
|
onPressed: () => _requestNewUserByUsername(
|
||||||
|
_usernameController.text,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 6),
|
||||||
|
child: IconButton.filled(
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
foregroundColor: context.color.onPrimary,
|
||||||
|
minimumSize: const Size(36, 36),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
),
|
||||||
|
iconSize: 18,
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||||
|
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||||
|
tooltip: context.lang.scanOtherProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: Column(
|
child: Row(
|
||||||
|
children: [
|
||||||
|
MyButton(
|
||||||
|
variant: MyButtonVariant.primaryDense,
|
||||||
|
onPressed: _shareProfile,
|
||||||
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
const FaIcon(FontAwesomeIcons.shareNodes, size: 14),
|
||||||
children: [
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Text(
|
||||||
child: FilledButton.icon(
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: primaryColor,
|
|
||||||
foregroundColor: Colors.black87,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal: 10,
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: _shareProfile,
|
|
||||||
icon: const FaIcon(
|
|
||||||
FontAwesomeIcons.shareNodes,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
context.lang.shareYourProfile,
|
context.lang.shareYourProfile,
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton.icon(
|
child: MyButton(
|
||||||
style: FilledButton.styleFrom(
|
variant: MyButtonVariant.secondaryDense,
|
||||||
backgroundColor: context.color.secondaryContainer,
|
|
||||||
foregroundColor: context.color.onSecondaryContainer,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal: 10,
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: _showMyQrCode,
|
onPressed: _showMyQrCode,
|
||||||
icon: const FaIcon(
|
child: Row(
|
||||||
FontAwesomeIcons.qrcode,
|
mainAxisSize: MainAxisSize.min,
|
||||||
size: 14,
|
children: [
|
||||||
),
|
const FaIcon(FontAwesomeIcons.qrcode, size: 14),
|
||||||
label: Text(
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
context.lang.openYourOwnQRcode,
|
context.lang.openYourOwnQRcode,
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,10 @@ class OpenRequestsListComp extends StatelessWidget {
|
||||||
if (block) {
|
if (block) {
|
||||||
const update = ContactsCompanion(blocked: Value(true));
|
const update = ContactsCompanion(blocked: Value(true));
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
await twonlyDB.contactsDao.updateContact(
|
||||||
|
contact.userId,
|
||||||
|
update,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -189,7 +192,9 @@ class OpenRequestsListComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: contact.requested ? requestedActions(context, contact) : sendRequestActions(context, contact),
|
children: contact.requested
|
||||||
|
? requestedActions(context, contact)
|
||||||
|
: sendRequestActions(context, contact),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,18 @@ class MemoriesFlashbackBannerComp extends StatelessWidget {
|
||||||
Image.file(
|
Image.file(
|
||||||
items.first.mediaService.storedPath,
|
items.first.mediaService.storedPath,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.broken_image_outlined,
|
||||||
|
color: Colors.white30,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
|
|
|
||||||
|
|
@ -79,20 +79,30 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp>
|
||||||
_scaleController.value = 1.0;
|
_scaleController.value = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
_listener = ImageStreamListener((info, _) {
|
_listener = ImageStreamListener(
|
||||||
|
(info, _) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_imageInfo = info;
|
_imageInfo = info;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError: (exception, stackTrace) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_imageProvider = null;
|
||||||
|
_imageInfo = null;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
_resolveImage();
|
_resolveImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resolveImage() {
|
void _resolveImage() {
|
||||||
final media = widget.galleryItem.mediaService;
|
final media = widget.galleryItem.mediaService;
|
||||||
final hasThumbnail = media.thumbnailPath.existsSync();
|
final hasThumbnail = media.thumbnailPath.existsSync() && media.thumbnailPath.lengthSync() > 0;
|
||||||
final hasStored = media.storedPath.existsSync();
|
final hasStored = media.storedPath.existsSync() && media.storedPath.lengthSync() > 0;
|
||||||
final isImageOrGif =
|
final isImageOrGif =
|
||||||
media.mediaFile.type == MediaType.image ||
|
media.mediaFile.type == MediaType.image ||
|
||||||
media.mediaFile.type == MediaType.gif;
|
media.mediaFile.type == MediaType.gif;
|
||||||
|
|
@ -181,6 +191,17 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp>
|
||||||
image: _imageProvider!,
|
image: _imageProvider!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
child: const Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.image,
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ColoredBox(
|
ColoredBox(
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,63 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showProgressDialog(
|
||||||
|
String message,
|
||||||
|
Future<void> Function(void Function(double progress) setProgress) task,
|
||||||
|
) async {
|
||||||
|
final progressNotifier = ValueNotifier<double>(0);
|
||||||
|
|
||||||
|
// Show non-dismissible progress dialog
|
||||||
|
// ignore: unawaited_futures
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: AlertDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ValueListenableBuilder<double>(
|
||||||
|
valueListenable: progressNotifier,
|
||||||
|
builder: (context, progress, _) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
LinearProgressIndicator(value: progress),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'${(progress * 100).toInt()}%',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
await task((p) => progressNotifier.value = p);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
progressNotifier.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _batchDelete() async {
|
Future<void> _batchDelete() async {
|
||||||
final count = _selectedMediaIds.length;
|
final count = _selectedMediaIds.length;
|
||||||
final confirmed = await showAlertDialog(
|
final confirmed = await showAlertDialog(
|
||||||
|
|
@ -204,7 +261,13 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
final items = _service.currentState.galleryItems;
|
final items = _service.currentState.galleryItems;
|
||||||
for (final mediaId in _selectedMediaIds) {
|
final selectedList = _selectedMediaIds.toList();
|
||||||
|
|
||||||
|
await _showProgressDialog(
|
||||||
|
'Deleting memories...',
|
||||||
|
(setProgress) async {
|
||||||
|
for (var i = 0; i < selectedList.length; i++) {
|
||||||
|
final mediaId = selectedList[i];
|
||||||
final item = items
|
final item = items
|
||||||
.where((e) => e.mediaService.mediaFile.mediaId == mediaId)
|
.where((e) => e.mediaService.mediaFile.mediaId == mediaId)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|
@ -212,7 +275,10 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
item.mediaService.fullMediaRemoval();
|
item.mediaService.fullMediaRemoval();
|
||||||
}
|
}
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
||||||
|
setProgress((i + 1) / selectedList.length);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
setState(_selectedMediaIds.clear);
|
setState(_selectedMediaIds.clear);
|
||||||
|
|
||||||
|
|
@ -226,9 +292,15 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
|
|
||||||
Future<void> _batchExport() async {
|
Future<void> _batchExport() async {
|
||||||
final items = _service.currentState.galleryItems;
|
final items = _service.currentState.galleryItems;
|
||||||
|
final selectedList = _selectedMediaIds.toList();
|
||||||
|
if (selectedList.isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (final mediaId in _selectedMediaIds) {
|
await _showProgressDialog(
|
||||||
|
'Exporting memories...',
|
||||||
|
(setProgress) async {
|
||||||
|
for (var i = 0; i < selectedList.length; i++) {
|
||||||
|
final mediaId = selectedList[i];
|
||||||
final item = items
|
final item = items
|
||||||
.where((e) => e.mediaService.mediaFile.mediaId == mediaId)
|
.where((e) => e.mediaService.mediaFile.mediaId == mediaId)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|
@ -242,7 +314,12 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
await saveImageToGallery(imageBytes, createdAt: media.mediaFile.createdAt);
|
await saveImageToGallery(imageBytes, createdAt: media.mediaFile.createdAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setProgress((i + 1) / selectedList.length);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(_selectedMediaIds.clear);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
showSnackbar(
|
showSnackbar(
|
||||||
|
|
@ -258,26 +335,36 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
|
|
||||||
Future<void> _batchFavorite() async {
|
Future<void> _batchFavorite() async {
|
||||||
final items = _service.currentState.galleryItems;
|
final items = _service.currentState.galleryItems;
|
||||||
|
final selectedList = _selectedMediaIds.toList();
|
||||||
|
if (selectedList.isEmpty) return;
|
||||||
|
|
||||||
var favCount = 0;
|
var favCount = 0;
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
if (_selectedMediaIds.contains(item.mediaService.mediaFile.mediaId)) {
|
if (selectedList.contains(item.mediaService.mediaFile.mediaId)) {
|
||||||
if (item.mediaService.mediaFile.isFavorite) {
|
if (item.mediaService.mediaFile.isFavorite) {
|
||||||
favCount++;
|
favCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final areAllFav =
|
final areAllFav =
|
||||||
_selectedMediaIds.isNotEmpty && favCount == _selectedMediaIds.length;
|
selectedList.isNotEmpty && favCount == selectedList.length;
|
||||||
final targetFav = !areAllFav;
|
final targetFav = !areAllFav;
|
||||||
|
|
||||||
for (final mediaId in _selectedMediaIds) {
|
await _showProgressDialog(
|
||||||
|
targetFav ? 'Adding to favorites...' : 'Removing from favorites...',
|
||||||
|
(setProgress) async {
|
||||||
|
for (var i = 0; i < selectedList.length; i++) {
|
||||||
|
final mediaId = selectedList[i];
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
mediaId,
|
mediaId,
|
||||||
MediaFilesCompanion(isFavorite: Value(targetFav)),
|
MediaFilesCompanion(isFavorite: Value(targetFav)),
|
||||||
);
|
);
|
||||||
|
setProgress((i + 1) / selectedList.length);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {});
|
setState(_selectedMediaIds.clear);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,15 @@ class _SynchronizedImageViewerScreenState
|
||||||
backgroundDecoration: const BoxDecoration(
|
backgroundDecoration: const BoxDecoration(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
),
|
),
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.broken_image_outlined,
|
||||||
|
color: Colors.white38,
|
||||||
|
size: 64,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
scaleStateChangedCallback: (state) {
|
scaleStateChangedCallback: (state) {
|
||||||
final zoomed =
|
final zoomed =
|
||||||
state != PhotoViewScaleState.initial;
|
state != PhotoViewScaleState.initial;
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
|
||||||
|
|
||||||
class OnboardingWrapper extends StatelessWidget {
|
|
||||||
const OnboardingWrapper({
|
|
||||||
required this.children,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final isDark = isDarkMode(context);
|
|
||||||
final backgroundColor = isDark ? const Color(0xFF0F172A) : primaryColor;
|
|
||||||
final topBlobColor = isDark
|
|
||||||
? primaryColor.withValues(alpha: 0.15)
|
|
||||||
: Colors.white.withValues(alpha: 0.1);
|
|
||||||
final bottomBlobColor = isDark
|
|
||||||
? primaryColor.withValues(alpha: 0.08)
|
|
||||||
: Colors.black.withValues(alpha: 0.05);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
top: -100,
|
|
||||||
right: -100,
|
|
||||||
child: Container(
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: topBlobColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -50,
|
|
||||||
left: -50,
|
|
||||||
child: Container(
|
|
||||||
width: 200,
|
|
||||||
height: 200,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: bottomBlobColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SafeArea(
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: constraints.maxHeight,
|
|
||||||
),
|
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:introduction_screen/introduction_screen.dart';
|
import 'package:introduction_screen/introduction_screen.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
class OnboardingView extends StatelessWidget {
|
class OnboardingView extends StatelessWidget {
|
||||||
const OnboardingView({required this.callbackOnSuccess, super.key});
|
const OnboardingView({required this.callbackOnSuccess, super.key});
|
||||||
|
|
@ -53,19 +54,6 @@ class OnboardingView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// PageViewModel(
|
|
||||||
// title: context.lang.onboardingSendTwonliesTitle,
|
|
||||||
// body: context.lang.onboardingSendTwonliesBody,
|
|
||||||
// image: Center(
|
|
||||||
// child: Padding(
|
|
||||||
// padding: const EdgeInsets.only(top: 100),
|
|
||||||
// child: Lottie.asset(
|
|
||||||
// 'assets/animations/twonlies.lottie',
|
|
||||||
// repeat: false,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
PageViewModel(
|
PageViewModel(
|
||||||
title: context.lang.onboardingNotProductTitle,
|
title: context.lang.onboardingNotProductTitle,
|
||||||
bodyWidget: Column(
|
bodyWidget: Column(
|
||||||
|
|
@ -81,7 +69,7 @@ class OnboardingView extends StatelessWidget {
|
||||||
right: 50,
|
right: 50,
|
||||||
top: 20,
|
top: 20,
|
||||||
),
|
),
|
||||||
child: FilledButton(
|
child: MyButton(
|
||||||
onPressed: callbackOnSuccess,
|
onPressed: callbackOnSuccess,
|
||||||
child: Text(context.lang.registerSubmitButton),
|
child: Text(context.lang.registerSubmitButton),
|
||||||
),
|
),
|
||||||
|
|
@ -97,17 +85,6 @@ class OnboardingView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// PageViewModel(
|
|
||||||
// title: context.lang.onboardingGetStartedTitle,
|
|
||||||
// image: Center(
|
|
||||||
// child: Padding(
|
|
||||||
// padding: const EdgeInsets.only(top: 100),
|
|
||||||
// child: Lottie.asset(
|
|
||||||
// 'assets/animations/rocket.lottie',
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
done: const Text(''),
|
done: const Text(''),
|
||||||
next: Text(context.lang.next),
|
next: Text(context.lang.next),
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:restart_app/restart_app.dart';
|
import 'package:restart_app/restart_app.dart';
|
||||||
import 'package:twonly/src/services/backup.service.dart';
|
import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/components/onboarding_wrapper.dart';
|
import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart';
|
||||||
|
|
||||||
class BackupRecoveryView extends StatefulWidget {
|
class BackupRecoveryView extends StatefulWidget {
|
||||||
const BackupRecoveryView({super.key});
|
const BackupRecoveryView({super.key});
|
||||||
|
|
@ -64,13 +64,30 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = isDarkMode(context);
|
final isDark = isDarkMode(context);
|
||||||
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
final titleColor = isDark ? Colors.white : Colors.black87;
|
||||||
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
final iconColor = isDark ? Colors.white70 : Colors.black54;
|
||||||
|
|
||||||
return OnboardingWrapper(
|
return GestureDetector(
|
||||||
|
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -79,29 +96,25 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.arrow_back_ios_new_rounded,
|
Icons.arrow_back_ios_new_rounded,
|
||||||
),
|
),
|
||||||
color: Colors.white,
|
color: iconColor,
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () => showBackupExplanation(context),
|
||||||
await showAlertDialog(
|
|
||||||
context,
|
|
||||||
'twonly Backup',
|
|
||||||
context.lang.backupTwonlySafeLongDesc,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||||
color: Colors.white,
|
color: iconColor,
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: LinkLogoAnimation(),
|
child: LinkLogoAnimation(
|
||||||
|
color: isDark ? Colors.white : Colors.black,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -110,83 +123,28 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.twonlySafeRecoverTitle,
|
context.lang.twonlySafeRecoverTitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
color: Colors.white,
|
color: titleColor,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
Container(
|
MyInput(
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: isDark
|
|
||||||
? Colors.black.withValues(alpha: 0.3)
|
|
||||||
: Colors.black.withValues(alpha: 0.1),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 10),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: usernameCtrl,
|
controller: usernameCtrl,
|
||||||
onChanged: (value) => setState(() {}),
|
onChanged: (value) => setState(() {}),
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isDark ? Colors.white : Colors.black,
|
|
||||||
),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: context.lang.registerUsernameDecoration,
|
hintText: context.lang.registerUsernameDecoration,
|
||||||
hintStyle: TextStyle(
|
prefixIcon: const Icon(Icons.alternate_email),
|
||||||
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: inputColor,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
prefixIcon: Icon(
|
|
||||||
Icons.alternate_email,
|
|
||||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
MyInput(
|
||||||
controller: passwordCtrl,
|
controller: passwordCtrl,
|
||||||
onChanged: (value) => setState(() {}),
|
onChanged: (value) => setState(() {}),
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isDark ? Colors.white : Colors.black,
|
|
||||||
),
|
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: context.lang.password,
|
hintText: context.lang.password,
|
||||||
hintStyle: TextStyle(
|
prefixIcon: const Icon(Icons.lock_outline_rounded),
|
||||||
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: inputColor,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
prefixIcon: Icon(
|
|
||||||
Icons.lock_outline_rounded,
|
|
||||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
|
||||||
),
|
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -198,46 +156,36 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
? FontAwesomeIcons.eye
|
? FontAwesomeIcons.eye
|
||||||
: FontAwesomeIcons.eyeSlash,
|
: FontAwesomeIcons.eyeSlash,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
FilledButton(
|
MyButton(
|
||||||
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: primaryColor,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
minimumSize: const Size.fromHeight(60),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(18),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
valueColor: AlwaysStoppedAnimation(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(context.lang.twonlySafeRecoverBtn),
|
||||||
context.lang.twonlySafeRecoverBtn,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/pow.dart';
|
import 'package:twonly/src/utils/pow.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/components/onboarding_wrapper.dart';
|
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
class RegisterView extends StatefulWidget {
|
class RegisterView extends StatefulWidget {
|
||||||
|
|
@ -43,8 +43,9 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
bool _registrationDisabled = false;
|
bool _registrationDisabled = false;
|
||||||
bool _isTryingToRegister = false;
|
bool _isTryingToRegister = false;
|
||||||
bool _isValidUserName = false;
|
bool _isValidUserName = false;
|
||||||
bool _showUserNameError = false;
|
|
||||||
bool _showProofOfWorkError = false;
|
bool _showProofOfWorkError = false;
|
||||||
|
String? _usernameErrorText;
|
||||||
|
|
||||||
late Future<int>? proofOfWork;
|
late Future<int>? proofOfWork;
|
||||||
|
|
||||||
|
|
@ -58,7 +59,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
Future<void> createNewUser() async {
|
Future<void> createNewUser() async {
|
||||||
if (!_isValidUserName) {
|
if (!_isValidUserName) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showUserNameError = true;
|
_usernameErrorText = context.lang.registerUsernameLimits;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -67,10 +68,11 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isTryingToRegister = true;
|
_isTryingToRegister = true;
|
||||||
_showUserNameError = false;
|
_usernameErrorText = null;
|
||||||
_showProofOfWorkError = false;
|
_showProofOfWorkError = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
late int proof;
|
late int proof;
|
||||||
|
|
||||||
if (proofOfWork != null) {
|
if (proofOfWork != null) {
|
||||||
|
|
@ -78,12 +80,14 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
} else {
|
} else {
|
||||||
final (pow, registrationDisabled) = await apiService.getProofOfWork();
|
final (pow, registrationDisabled) = await apiService.getProofOfWork();
|
||||||
if (pow == null) {
|
if (pow == null) {
|
||||||
|
setState(() {
|
||||||
_registrationDisabled = registrationDisabled;
|
_registrationDisabled = registrationDisabled;
|
||||||
|
_isTryingToRegister = false;
|
||||||
|
});
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showNetworkIssue(context);
|
showNetworkIssue(context);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
// Starting with the proof of work.
|
|
||||||
}
|
}
|
||||||
proof = await calculatePoW(pow.prefix, pow.difficulty.toInt());
|
proof = await calculatePoW(pow.prefix, pow.difficulty.toInt());
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +105,10 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
} else {
|
} else {
|
||||||
proofOfWork = null;
|
proofOfWork = null;
|
||||||
if (res.error == ErrorCode.RegistrationDisabled) {
|
if (res.error == ErrorCode.RegistrationDisabled) {
|
||||||
|
setState(() {
|
||||||
_registrationDisabled = true;
|
_registrationDisabled = true;
|
||||||
|
_isTryingToRegister = false;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
||||||
|
|
@ -109,6 +116,17 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
await deleteLocalUserData();
|
await deleteLocalUserData();
|
||||||
return createNewUser();
|
return createNewUser();
|
||||||
}
|
}
|
||||||
|
if (res.error == ErrorCode.UsernameAlreadyTaken ||
|
||||||
|
res.error == ErrorCode.UsernameNotValid) {
|
||||||
|
setState(() {
|
||||||
|
_usernameErrorText = errorCodeToText(
|
||||||
|
context,
|
||||||
|
res.error as ErrorCode,
|
||||||
|
);
|
||||||
|
_isTryingToRegister = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res.error == ErrorCode.InvalidProofOfWork) {
|
if (res.error == ErrorCode.InvalidProofOfWork) {
|
||||||
await deleteLocalUserData();
|
await deleteLocalUserData();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -147,23 +165,50 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
|
|
||||||
await apiService.authenticate();
|
await apiService.authenticate();
|
||||||
widget.callbackOnSuccess();
|
widget.callbackOnSuccess();
|
||||||
|
} catch (e, stack) {
|
||||||
|
Log.error('Error creating new user', e, stack);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isTryingToRegister = false;
|
||||||
|
});
|
||||||
|
await showAlertDialog(
|
||||||
|
context,
|
||||||
|
'Error',
|
||||||
|
e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = isDarkMode(context);
|
final isDark = isDarkMode(context);
|
||||||
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
|
||||||
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
|
||||||
final sloganColor = isDark ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800];
|
|
||||||
final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600];
|
|
||||||
|
|
||||||
return OnboardingWrapper(
|
return GestureDetector(
|
||||||
|
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: const LinkLogoAnimation(),
|
child: LinkLogoAnimation(
|
||||||
|
color: isDark ? Colors.white : Colors.black,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -174,61 +219,55 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
color:
|
||||||
|
Theme.of(context).textTheme.bodyMedium?.color
|
||||||
|
?.withValues(alpha: 0.7) ??
|
||||||
|
(isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.7)
|
||||||
|
: Colors.black.withValues(alpha: 0.7)),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 40),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 10),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
if (_registrationDisabled) ...[
|
if (_registrationDisabled) ...[
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
Text(
|
||||||
context.lang.registrationClosed,
|
context.lang.registrationClosed,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.red,
|
color: Colors.redAccent,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 40),
|
||||||
] else ...[
|
] else ...[
|
||||||
Text(
|
Text(
|
||||||
context.lang.registerUsernameSlogan,
|
context.lang.registerUsernameSlogan,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 22,
|
||||||
color: sloganColor,
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: -0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 24),
|
||||||
TextField(
|
MyInput(
|
||||||
controller: usernameController,
|
controller: usernameController,
|
||||||
|
errorText: _usernameErrorText,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
usernameController.text = value.toLowerCase();
|
usernameController.text = value.toLowerCase();
|
||||||
usernameController.selection = TextSelection.fromPosition(
|
usernameController.selection =
|
||||||
|
TextSelection.fromPosition(
|
||||||
TextPosition(
|
TextPosition(
|
||||||
offset: usernameController.text.length,
|
offset: usernameController.text.length,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isValidUserName = usernameController.text.length >= 3;
|
_isValidUserName =
|
||||||
|
usernameController.text.length >= 3;
|
||||||
|
_usernameErrorText = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
|
|
@ -237,108 +276,63 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
RegExp('[a-z0-9A-Z._]'),
|
RegExp('[a-z0-9A-Z._]'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isDark ? Colors.white : Colors.black,
|
|
||||||
),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: context.lang.registerUsernameDecoration,
|
hintText: context.lang.registerUsernameDecoration,
|
||||||
hintStyle: TextStyle(
|
prefixIcon: const Icon(Icons.alternate_email),
|
||||||
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
|
||||||
),
|
),
|
||||||
filled: true,
|
|
||||||
fillColor: inputColor,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
prefixIcon: Icon(
|
|
||||||
Icons.alternate_email,
|
|
||||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_showUserNameError && usernameController.text.length < 3) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
context.lang.registerUsernameLimits,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (_showProofOfWorkError) ...[
|
if (_showProofOfWorkError) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
context.lang.registerProofOfWorkFailed,
|
context.lang.registerProofOfWorkFailed,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.red,
|
color: Colors.redAccent,
|
||||||
fontSize: 13,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 32),
|
||||||
FilledButton(
|
MyButton(
|
||||||
onPressed: _isTryingToRegister ? null : createNewUser,
|
onPressed: _isTryingToRegister
|
||||||
style: FilledButton.styleFrom(
|
? null
|
||||||
backgroundColor: primaryColor,
|
: createNewUser,
|
||||||
foregroundColor: Colors.white,
|
|
||||||
minimumSize: const Size.fromHeight(60),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(18),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: _isTryingToRegister
|
child: _isTryingToRegister
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
valueColor: AlwaysStoppedAnimation(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
context.lang.registerSubmitButton,
|
context.lang.registerSubmitButton,
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
],
|
||||||
TextButton(
|
MyButton(
|
||||||
onPressed: () => context.push(
|
onPressed: () => context.push(
|
||||||
Routes.settingsBackupRecovery,
|
Routes.settingsBackupRecovery,
|
||||||
),
|
),
|
||||||
style: TextButton.styleFrom(
|
variant: MyButtonVariant.secondary,
|
||||||
minimumSize: const Size.fromHeight(50),
|
|
||||||
foregroundColor: secondaryButtonColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.twonlySafeRecoverBtn,
|
context.lang.twonlySafeRecoverBtn,
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/profile.service.dart';
|
import 'package:twonly/src/services/profile.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/backup.setup.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/backup.setup.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart';
|
||||||
|
|
@ -152,7 +153,9 @@ class _SetupViewState extends State<SetupView> {
|
||||||
right: index == currentPage.totalPages - 1 ? 0 : 8,
|
right: index == currentPage.totalPages - 1 ? 0 : 8,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isFinished ? context.color.primary : context.color.surfaceContainer,
|
color: isFinished
|
||||||
|
? context.color.primary
|
||||||
|
: context.color.surfaceContainer,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -175,35 +178,30 @@ class _SetupViewState extends State<SetupView> {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (currentPage.index > 0)
|
if (currentPage.index > 0)
|
||||||
TextButton(
|
MyButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u.currentSetupPage = currentPage.previous()?.name;
|
u.currentSetupPage = currentPage.previous()?.name;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
variant: MyButtonVariant.text,
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.back,
|
context.lang.back,
|
||||||
style: TextStyle(
|
|
||||||
color: context.color.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (currentPage.index > 0 && !currentPage.isLast)
|
||||||
if (currentPage.index > 0 && !currentPage.isLast) const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
if (!currentPage.isLast)
|
if (!currentPage.isLast)
|
||||||
TextButton(
|
MyButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u.skipSetupPages = true,
|
(u) => u.skipSetupPages = true,
|
||||||
);
|
);
|
||||||
widget.onUpdate?.call();
|
widget.onUpdate?.call();
|
||||||
},
|
},
|
||||||
|
variant: MyButtonVariant.text,
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.onboardingFinishLater,
|
context.lang.onboardingFinishLater,
|
||||||
style: TextStyle(
|
|
||||||
color: context.color.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
class FinishSetupComp extends StatefulWidget {
|
class FinishSetupComp extends StatefulWidget {
|
||||||
|
|
@ -123,29 +124,19 @@ class _FinishSetupCompState extends State<FinishSetupComp> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
FilledButton.icon(
|
MyButton(
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
icon: const Icon(
|
variant: MyButtonVariant.primaryDense,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
Icons.arrow_forward_rounded,
|
Icons.arrow_forward_rounded,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
label: Text(
|
const SizedBox(width: 8),
|
||||||
context.lang.finishSetupCardAction,
|
Text(context.lang.finishSetupCardAction),
|
||||||
style: const TextStyle(
|
],
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: context.color.primary,
|
|
||||||
foregroundColor: context.color.onPrimary,
|
|
||||||
minimumSize: const Size(0, 40),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
class NextButtonComp extends StatelessWidget {
|
class NextButtonComp extends StatelessWidget {
|
||||||
|
|
@ -24,7 +25,7 @@ class NextButtonComp extends StatelessWidget {
|
||||||
final currentPage = SetupPagesExtension.fromStr(
|
final currentPage = SetupPagesExtension.fromStr(
|
||||||
userService.currentUser.currentSetupPage,
|
userService.currentUser.currentSetupPage,
|
||||||
);
|
);
|
||||||
return ElevatedButton(
|
return MyButton(
|
||||||
onPressed: (canSubmit && !isLoading)
|
onPressed: (canSubmit && !isLoading)
|
||||||
? () async {
|
? () async {
|
||||||
if (onPressed != null) {
|
if (onPressed != null) {
|
||||||
|
|
@ -36,15 +37,6 @@ class NextButtonComp extends StatelessWidget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: const Size(double.infinity, 56),
|
|
||||||
backgroundColor: context.color.primary,
|
|
||||||
foregroundColor: context.color.onPrimary,
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
|
|
@ -56,7 +48,6 @@ class NextButtonComp extends StatelessWidget {
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
currentPage.isLast ? context.lang.finishSetup : context.lang.next,
|
currentPage.isLast ? context.lang.finishSetup : context.lang.next,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import 'package:avatar_maker/avatar_maker.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'
|
||||||
|
show AvatarIcon;
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
|
||||||
import 'package:vector_graphics/vector_graphics.dart';
|
|
||||||
|
|
||||||
class ProfileSetupPage extends StatefulWidget {
|
class ProfileSetupPage extends StatefulWidget {
|
||||||
const ProfileSetupPage({super.key});
|
const ProfileSetupPage({super.key});
|
||||||
|
|
@ -17,10 +19,7 @@ class ProfileSetupPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||||
final AvatarMakerController _avatarMakerController =
|
|
||||||
PersistentAvatarMakerController(customizedPropertyCategories: []);
|
|
||||||
late final TextEditingController _displayNameController;
|
late final TextEditingController _displayNameController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -35,6 +34,9 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder(
|
||||||
|
stream: userService.onUserUpdated,
|
||||||
|
builder: (context, asyncSnapshot) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -58,59 +60,45 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||||
builder: (context, asyncSnapshot) {
|
builder: (context, asyncSnapshot) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: context.color.primary.withValues(alpha: 0.2),
|
color: primaryColor,
|
||||||
width: 4,
|
width: 4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: userService.currentUser.avatarSvg == null
|
child: const AvatarIcon(
|
||||||
? ClipRRect(
|
fontSize: 68,
|
||||||
borderRadius: BorderRadius.circular(80),
|
myAvatar: true,
|
||||||
child: Container(
|
|
||||||
width: 160,
|
|
||||||
height: 160,
|
|
||||||
color: context.color.surfaceContainer,
|
|
||||||
child: const SvgPicture(
|
|
||||||
AssetBytesLoader(
|
|
||||||
'assets/images/default_avatar.svg.vec',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: AvatarMakerAvatar(
|
|
||||||
backgroundColor: context.color.surfaceContainer,
|
|
||||||
radius: 80,
|
|
||||||
controller: _avatarMakerController,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextButton.icon(
|
MyButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await context.push(Routes.settingsProfileModifyAvatar);
|
await context.push(Routes.settingsProfileModifyAvatar);
|
||||||
await _avatarMakerController.performRestore();
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.palette_outlined),
|
variant: MyButtonVariant.text,
|
||||||
label: Text(context.lang.settingsProfileCustomizeAvatar),
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.palette_outlined),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.lang.settingsProfileCustomizeAvatar),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
TextField(
|
MyInput(
|
||||||
controller: _displayNameController,
|
controller: _displayNameController,
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: context.lang.settingsProfileEditDisplayName,
|
|
||||||
hintText: context.lang.settingsProfileEditDisplayNameNew,
|
hintText: context.lang.settingsProfileEditDisplayNameNew,
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
filled: true,
|
|
||||||
fillColor: context.color.surfaceContainerLow,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
NextButtonComp(
|
NextButtonComp(
|
||||||
|
|
@ -125,5 +113,7 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/model/json/backup.model.dart';
|
import 'package:twonly/src/model/json/backup.model.dart';
|
||||||
import 'package:twonly/src/services/backup.service.dart';
|
import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
class BackupView extends StatefulWidget {
|
class BackupView extends StatefulWidget {
|
||||||
const BackupView({super.key});
|
const BackupView({super.key});
|
||||||
|
|
@ -176,7 +177,8 @@ class _BackupViewState extends State<BackupView> {
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
OutlinedButton(
|
MyButton(
|
||||||
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
onPressed: _isLoading
|
onPressed: _isLoading
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
|
|
@ -194,7 +196,8 @@ class _BackupViewState extends State<BackupView> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Center(
|
Center(
|
||||||
child: FilledButton(
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryDense,
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
context.push(Routes.settingsBackupSetup, extra: true),
|
context.push(Routes.settingsBackupSetup, extra: true),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart';
|
import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart';
|
||||||
|
|
||||||
class SetupBackupView extends StatefulWidget {
|
class SetupBackupView extends StatefulWidget {
|
||||||
|
|
@ -76,13 +77,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
title: const Text('twonly Backup'),
|
title: const Text('twonly Backup'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () => showBackupExplanation(context),
|
||||||
await showAlertDialog(
|
|
||||||
context,
|
|
||||||
'twonly Backup',
|
|
||||||
context.lang.backupTwonlySafeLongDesc,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
),
|
),
|
||||||
|
|
@ -131,7 +126,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Center(
|
Center(
|
||||||
child: FilledButton.icon(
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
onPressed:
|
onPressed:
|
||||||
(!_isLoading &&
|
(!_isLoading &&
|
||||||
(_passwordController.text ==
|
(_passwordController.text ==
|
||||||
|
|
@ -140,18 +136,27 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
!kReleaseMode))
|
!kReleaseMode))
|
||||||
? _updateBackupPassword
|
? _updateBackupPassword
|
||||||
: null,
|
: null,
|
||||||
icon: _isLoading
|
child: Row(
|
||||||
? const SizedBox(
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_isLoading)
|
||||||
|
const SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
width: 12,
|
width: 12,
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.lock_clock_rounded),
|
else
|
||||||
label: Text(
|
const Icon(Icons.lock_clock_rounded),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
userService.currentUser.isBackupEnabled
|
userService.currentUser.isBackupEnabled
|
||||||
? context.lang.backupEnableBackup
|
? context.lang.backupEnableBackup
|
||||||
: context.lang.backupChangePassword,
|
: context.lang.backupChangePassword,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
|
|
||||||
Future<bool> isSecurePassword(String password) async {
|
Future<bool> isSecurePassword(String password) async {
|
||||||
final badPasswordsStr = await rootBundle.loadString(
|
final badPasswordsStr = await rootBundle.loadString(
|
||||||
|
|
@ -46,19 +48,11 @@ class _BackupPasswordTextFieldState extends State<BackupPasswordTextField> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextField(
|
return MyInput(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
obscureText: _obscureText,
|
obscureText: _obscureText,
|
||||||
decoration: InputDecoration(
|
hintText: widget.labelText,
|
||||||
labelText: widget.labelText,
|
|
||||||
filled: true,
|
|
||||||
fillColor: context.color.surfaceContainerLow,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -70,7 +64,6 @@ class _BackupPasswordTextFieldState extends State<BackupPasswordTextField> {
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,3 +92,68 @@ class PasswordRequirementText extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showBackupExplanation(BuildContext context) {
|
||||||
|
final isDark = isDarkMode(context);
|
||||||
|
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
|
||||||
|
final textColor = isDark ? Colors.white : Colors.black87;
|
||||||
|
final subtitleColor = isDark ? Colors.white70 : Colors.black54;
|
||||||
|
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(28),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 5,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? Colors.white24 : Colors.black12,
|
||||||
|
borderRadius: BorderRadius.circular(2.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'twonly Backup',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
context.lang.backupTwonlySafeLongDesc,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.5,
|
||||||
|
color: subtitleColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
MyButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Got it'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,8 +297,8 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('User ID'),
|
title: const Text('Informations'),
|
||||||
subtitle: Text(userService.currentUser.userId.toString()),
|
onTap: () => context.push(Routes.settingsDeveloperInformations),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Show Retransmission Database'),
|
title: const Text('Show Retransmission Database'),
|
||||||
|
|
@ -343,24 +343,6 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
onChanged: (a) => toggleVideoStabilization(),
|
onChanged: (a) => toggleVideoStabilization(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
title: const Text('Delete all (!) app data'),
|
|
||||||
onTap: () async {
|
|
||||||
final ok = await showAlertDialog(
|
|
||||||
context,
|
|
||||||
'Sure?',
|
|
||||||
'If you do not have a backup, you have to register with a new account.',
|
|
||||||
);
|
|
||||||
if (ok) {
|
|
||||||
await deleteLocalUserData();
|
|
||||||
await Restart.restartApp(
|
|
||||||
notificationTitle: 'Account successfully deleted',
|
|
||||||
notificationBody: 'Click here to open the app again',
|
|
||||||
forceKill: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Reduce flames'),
|
title: const Text('Reduce flames'),
|
||||||
onTap: () => context.push(Routes.settingsDeveloperReduceFlames),
|
onTap: () => context.push(Routes.settingsDeveloperReduceFlames),
|
||||||
|
|
@ -400,7 +382,9 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
onTap: _isGeneratingMockImages
|
onTap: _isGeneratingMockImages
|
||||||
|
|
@ -415,6 +399,27 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Delete all app data',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final ok = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
'Sure?',
|
||||||
|
'If you do not have a backup, you have to register with a new account.',
|
||||||
|
);
|
||||||
|
if (ok) {
|
||||||
|
await deleteLocalUserData();
|
||||||
|
await Restart.restartApp(
|
||||||
|
notificationTitle: 'Account successfully deleted',
|
||||||
|
notificationBody: 'Click here to open the app again',
|
||||||
|
forceKill: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
107
lib/src/visual/views/settings/developer/informations.view.dart
Normal file
107
lib/src/visual/views/settings/developer/informations.view.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
|
|
||||||
|
class DeveloperInformationsView extends StatefulWidget {
|
||||||
|
const DeveloperInformationsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DeveloperInformationsView> createState() =>
|
||||||
|
_DeveloperInformationsViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
||||||
|
String? _lastFcmTimestamp;
|
||||||
|
String? _lastServerTimestamp;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadInformations();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadInformations({bool showFeedback = false}) async {
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
try {
|
||||||
|
final lastFcm = await storage.read(
|
||||||
|
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final lastServer = await storage.read(
|
||||||
|
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_lastFcmTimestamp = lastFcm;
|
||||||
|
_lastServerTimestamp = lastServer;
|
||||||
|
});
|
||||||
|
if (showFeedback) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
'Developer information loaded',
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTimestamp(String? timestampStr) {
|
||||||
|
if (timestampStr == null) return 'Never';
|
||||||
|
final ms = int.tryParse(timestampStr);
|
||||||
|
if (ms == null) return 'Invalid: $timestampStr';
|
||||||
|
final dt = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||||
|
return dt.toLocal().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userId = userService.currentUser.userId.toString();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Informations'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () => _loadInformations(showFeedback: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: const Text('User ID'),
|
||||||
|
subtitle: Text(userId),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.copy),
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: userId));
|
||||||
|
showSnackbar(context, 'User ID copied to clipboard');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Last FCM Message'),
|
||||||
|
subtitle: Text(_formatTimestamp(_lastFcmTimestamp)),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Last Server Message'),
|
||||||
|
subtitle: Text(_formatTimestamp(_lastServerTimestamp)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -12,6 +15,7 @@ import 'package:twonly/src/services/api/utils.api.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/contact_us/submit_message.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/contact_us/submit_message.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
||||||
|
|
||||||
|
|
@ -29,13 +33,29 @@ class _ContactUsState extends State<ContactUsView> {
|
||||||
int? _selectedFeedback;
|
int? _selectedFeedback;
|
||||||
String? _selectedReason;
|
String? _selectedReason;
|
||||||
String? debugLogDownloadToken;
|
String? debugLogDownloadToken;
|
||||||
|
String? debugLogEncryptionKey;
|
||||||
|
|
||||||
Future<String?> uploadDebugLog() async {
|
Future<(String, String)?> uploadDebugLog() async {
|
||||||
if (debugLogDownloadToken != null) return debugLogDownloadToken;
|
if (debugLogDownloadToken != null && debugLogEncryptionKey != null) {
|
||||||
|
return (debugLogDownloadToken!, debugLogEncryptionKey!);
|
||||||
|
}
|
||||||
final downloadToken = getRandomUint8List(32);
|
final downloadToken = getRandomUint8List(32);
|
||||||
|
final encryptionKey = getRandomUint8List(32);
|
||||||
|
|
||||||
final debugLog = await loadLogFile();
|
final debugLog = await loadLogFile();
|
||||||
|
|
||||||
|
// 1. Compress the debug log
|
||||||
|
final logBytes = utf8.encode(debugLog);
|
||||||
|
final compressedBytes = gzip.encode(logBytes);
|
||||||
|
|
||||||
|
// 2. Encrypt using AES-GCM (with 256 bits)
|
||||||
|
final algorithm = AesGcm.with256bits();
|
||||||
|
final secretBox = await algorithm.encrypt(
|
||||||
|
compressedBytes,
|
||||||
|
secretKey: SecretKey(encryptionKey),
|
||||||
|
);
|
||||||
|
final encryptedData = secretBox.concatenation();
|
||||||
|
|
||||||
final messageOnSuccess = TextMessage()
|
final messageOnSuccess = TextMessage()
|
||||||
..body = []
|
..body = []
|
||||||
..userId = Int64();
|
..userId = Int64();
|
||||||
|
|
@ -43,7 +63,7 @@ class _ContactUsState extends State<ContactUsView> {
|
||||||
final uploadRequest = UploadRequest(
|
final uploadRequest = UploadRequest(
|
||||||
messagesOnSuccess: [messageOnSuccess],
|
messagesOnSuccess: [messageOnSuccess],
|
||||||
downloadTokens: [downloadToken],
|
downloadTokens: [downloadToken],
|
||||||
encryptedData: debugLog.codeUnits,
|
encryptedData: encryptedData,
|
||||||
);
|
);
|
||||||
|
|
||||||
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
||||||
|
|
@ -71,10 +91,13 @@ class _ContactUsState extends State<ContactUsView> {
|
||||||
|
|
||||||
final response = await requestMultipart.send();
|
final response = await requestMultipart.send();
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
final tokenHex = uint8ListToHex(downloadToken);
|
||||||
|
final keyHex = uint8ListToHex(encryptionKey);
|
||||||
setState(() {
|
setState(() {
|
||||||
debugLogDownloadToken = uint8ListToHex(downloadToken);
|
debugLogDownloadToken = tokenHex;
|
||||||
|
debugLogEncryptionKey = keyHex;
|
||||||
});
|
});
|
||||||
return debugLogDownloadToken;
|
return (tokenHex, keyHex);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -108,13 +131,13 @@ class _ContactUsState extends State<ContactUsView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeDebugLog) {
|
if (includeDebugLog) {
|
||||||
String? token;
|
(String, String)? result;
|
||||||
try {
|
try {
|
||||||
token = await uploadDebugLog();
|
result = await uploadDebugLog();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
}
|
}
|
||||||
if (token == null) {
|
if (result == null) {
|
||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
showSnackbar(context, 'Could not upload the debug log!');
|
showSnackbar(context, 'Could not upload the debug log!');
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -122,7 +145,10 @@ class _ContactUsState extends State<ContactUsView> {
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
debugLogToken = 'Debug Log: https://api.twonly.eu/api/download/$token';
|
final downloadToken = result.$1;
|
||||||
|
final encryptionKey = result.$2;
|
||||||
|
debugLogToken =
|
||||||
|
'Debug Log: https://logs.twonly.eu#$downloadToken/$encryptionKey';
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -238,17 +264,8 @@ $debugLogToken
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
MyButton(
|
||||||
icon: isLoading
|
variant: MyButtonVariant.primaryDense,
|
||||||
? SizedBox(
|
|
||||||
height: 12,
|
|
||||||
width: 12,
|
|
||||||
child: CircularProgressIndicator.adaptive(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.inversePrimary),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const FaIcon(FontAwesomeIcons.angleRight),
|
|
||||||
onPressed: isLoading
|
onPressed: isLoading
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
|
|
@ -263,7 +280,24 @@ $debugLogToken
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: Text(context.lang.next),
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isLoading)
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
width: 12,
|
||||||
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation(Colors.black87),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const FaIcon(FontAwesomeIcons.angleRight, size: 14),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.lang.next),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
class SubmitMessage extends StatefulWidget {
|
class SubmitMessage extends StatefulWidget {
|
||||||
const SubmitMessage({required this.fullMessage, super.key});
|
const SubmitMessage({required this.fullMessage, super.key});
|
||||||
|
|
@ -100,7 +101,8 @@ class _ContactUsState extends State<SubmitMessage> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
MyButton(
|
||||||
|
variant: MyButtonVariant.primaryDense,
|
||||||
onPressed: isLoading ? null : _submitFeedback,
|
onPressed: isLoading ? null : _submitFeedback,
|
||||||
child: Text(context.lang.submit),
|
child: Text(context.lang.submit),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,8 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final showShareYourFriends =
|
final showShareYourFriends =
|
||||||
showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.shareYourFriends;
|
showOnlySpecificPage == UserDiscoveryPages.all ||
|
||||||
|
showOnlySpecificPage == UserDiscoveryPages.shareYourFriends;
|
||||||
final showLetYourFriendsFindYou =
|
final showLetYourFriendsFindYou =
|
||||||
showOnlySpecificPage == UserDiscoveryPages.all ||
|
showOnlySpecificPage == UserDiscoveryPages.all ||
|
||||||
showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou;
|
showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou;
|
||||||
|
|
@ -336,7 +337,9 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
context.lang.userDiscoverySettingsManualApprovalDesc,
|
context
|
||||||
|
.lang
|
||||||
|
.userDiscoverySettingsManualApprovalDesc,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: context.color.onSurfaceVariant,
|
color: context.color.onSurfaceVariant,
|
||||||
|
|
@ -350,13 +353,16 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
crossFadeState: state.isUserDiscoveryEnabled ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
crossFadeState: state.isUserDiscoveryEnabled
|
||||||
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showOnlySpecificPage == UserDiscoveryPages.all) const SizedBox(height: 48),
|
if (showOnlySpecificPage == UserDiscoveryPages.all)
|
||||||
|
const SizedBox(height: 48),
|
||||||
],
|
],
|
||||||
if (showLetYourFriendsFindYou) ...[
|
if (showLetYourFriendsFindYou) ...[
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -587,7 +593,9 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.userDiscoverySettingsMutualFriends,
|
context
|
||||||
|
.lang
|
||||||
|
.userDiscoverySettingsMutualFriends,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -603,7 +611,8 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
color: context.color.surface,
|
color: context.color.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: context.color.outlineVariant.withValues(
|
color: context.color.outlineVariant
|
||||||
|
.withValues(
|
||||||
alpha: 0.5,
|
alpha: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -616,9 +625,9 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
items: List.generate(
|
items: List.generate(
|
||||||
9,
|
8,
|
||||||
(index) {
|
(index) {
|
||||||
final value = index + 2;
|
final value = index + 3;
|
||||||
return DropdownMenuItem<int>(
|
return DropdownMenuItem<int>(
|
||||||
value: value,
|
value: value,
|
||||||
child: Text('$value'),
|
child: Text('$value'),
|
||||||
|
|
@ -640,7 +649,9 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
crossFadeState: state.sharePromotion ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
crossFadeState: state.sharePromotion
|
||||||
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
|
||||||
isUserDiscoveryEnabled: u.isUserDiscoveryEnabled,
|
isUserDiscoveryEnabled: u.isUserDiscoveryEnabled,
|
||||||
sharePromotion: u.userDiscoverySharePromotion,
|
sharePromotion: u.userDiscoverySharePromotion,
|
||||||
isManualApprovalEnabled: u.userDiscoveryRequiresManualApproval,
|
isManualApprovalEnabled: u.userDiscoveryRequiresManualApproval,
|
||||||
threshold: u.userDiscoveryThreshold,
|
threshold: u.userDiscoveryThreshold < 3 ? 3 : u.userDiscoveryThreshold,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
class ModifyAvatarView extends StatefulWidget {
|
class ModifyAvatarView extends StatefulWidget {
|
||||||
const ModifyAvatarView({super.key});
|
const ModifyAvatarView({super.key});
|
||||||
|
|
@ -15,12 +16,19 @@ class ModifyAvatarView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
final AvatarMakerController _avatarMakerController =
|
late final _CustomAvatarMakerController _avatarMakerController;
|
||||||
PersistentAvatarMakerController(customizedPropertyCategories: []);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
final svg = userService.currentUser.avatarSvg;
|
||||||
|
if (svg != null && svg.isNotEmpty) {
|
||||||
|
_avatarMakerController = _CustomAvatarMakerController(
|
||||||
|
svg: svg,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_avatarMakerController = _CustomAvatarMakerController.defaultAvatar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateUserAvatar(String json, String svg) async {
|
Future<void> updateUserAvatar(String json, String svg) async {
|
||||||
|
|
@ -33,49 +41,38 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarMakerThemeData getAvatarMakerTheme(BuildContext context) {
|
AvatarMakerThemeData getAvatarMakerTheme(BuildContext context) {
|
||||||
if (isDarkMode(context)) {
|
final colors = context.color;
|
||||||
|
final isDark = isDarkMode(context);
|
||||||
return AvatarMakerThemeData(
|
return AvatarMakerThemeData(
|
||||||
boxDecoration: const BoxDecoration(
|
boxDecoration: BoxDecoration(
|
||||||
boxShadow: [BoxShadow()],
|
color: colors.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -5),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
unselectedTileDecoration: BoxDecoration(
|
unselectedTileDecoration: BoxDecoration(
|
||||||
color: const Color.fromARGB(255, 50, 50, 50), // Dark mode color
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
selectedTileDecoration: BoxDecoration(
|
selectedTileDecoration: BoxDecoration(
|
||||||
color: const Color.fromARGB(255, 100, 100, 100), // Dark mode color
|
color: colors.primary.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(10),
|
border: Border.all(color: colors.primary, width: 2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
selectedIconColor: colors.primary,
|
||||||
|
unselectedIconColor: colors.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
|
primaryBgColor: colors.surface,
|
||||||
|
secondaryBgColor: colors.surfaceContainerLow,
|
||||||
|
labelTextStyle: TextStyle(
|
||||||
|
color: colors.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
selectedIconColor: Colors.white,
|
|
||||||
unselectedIconColor: Colors.grey,
|
|
||||||
primaryBgColor: Colors.black, // Dark mode background
|
|
||||||
secondaryBgColor: Colors.grey[850], // Dark mode secondary background
|
|
||||||
labelTextStyle: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
), // Light text for dark mode
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return AvatarMakerThemeData(
|
|
||||||
boxDecoration: const BoxDecoration(
|
|
||||||
boxShadow: [BoxShadow()],
|
|
||||||
),
|
|
||||||
unselectedTileDecoration: BoxDecoration(
|
|
||||||
color: const Color.fromARGB(255, 240, 240, 240), // Light mode color
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
selectedTileDecoration: BoxDecoration(
|
|
||||||
color: const Color.fromARGB(255, 200, 200, 200), // Light mode color
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
selectedIconColor: Colors.black,
|
|
||||||
unselectedIconColor: Colors.grey,
|
|
||||||
primaryBgColor: Colors.white, // Light mode background
|
|
||||||
secondaryBgColor: Colors.grey[200], // Light mode secondary background
|
|
||||||
labelTextStyle: const TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
), // Dark text for light mode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> _showBackDialog() {
|
Future<bool?> _showBackDialog() {
|
||||||
|
|
@ -148,39 +145,64 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
controller: _avatarMakerController,
|
controller: _avatarMakerController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 10,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
MyButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.floppyDisk),
|
variant: MyButtonVariant.primaryDense,
|
||||||
onPressed: storeAvatarAndExit,
|
onPressed: storeAvatarAndExit,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const FaIcon(FontAwesomeIcons.floppyDisk, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(context.lang.avatarSaveChangesStore),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
),
|
||||||
icon: const FaIcon(FontAwesomeIcons.shuffle),
|
MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryDense,
|
||||||
onPressed:
|
onPressed:
|
||||||
_avatarMakerController.randomizedSelectedOptions,
|
_avatarMakerController.randomizedSelectedOptions,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const FaIcon(FontAwesomeIcons.shuffle, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(context.lang.avatarCustomizeRandomize),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
),
|
||||||
icon: const FaIcon(FontAwesomeIcons.rotateLeft),
|
MyButton(
|
||||||
onLongPress: () async {
|
variant: MyButtonVariant.secondaryDense,
|
||||||
await PersistentAvatarMakerController.clearAvatarMaker();
|
|
||||||
await _avatarMakerController.restoreState();
|
|
||||||
},
|
|
||||||
onPressed: _avatarMakerController.restoreState,
|
onPressed: _avatarMakerController.restoreState,
|
||||||
|
onLongPress: () {
|
||||||
|
_avatarMakerController.clearCustomizations();
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const FaIcon(FontAwesomeIcons.rotateLeft, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(context.lang.avatarCustomizeReset),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
|
||||||
vertical: 30,
|
vertical: 30,
|
||||||
),
|
),
|
||||||
child: AvatarMakerCustomizer(
|
child: AvatarMakerCustomizer(
|
||||||
scaffoldWidth: min(
|
scaffoldWidth: min(
|
||||||
600,
|
600,
|
||||||
MediaQuery.of(context).size.width * 0.85,
|
MediaQuery.of(context).size.width * 0.95,
|
||||||
),
|
),
|
||||||
theme: getAvatarMakerTheme(context),
|
theme: getAvatarMakerTheme(context),
|
||||||
controller: _avatarMakerController,
|
controller: _avatarMakerController,
|
||||||
|
|
@ -194,3 +216,72 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CustomAvatarMakerController extends NonPersistentAvatarMakerController {
|
||||||
|
_CustomAvatarMakerController({
|
||||||
|
required super.svg,
|
||||||
|
}) : _initialSvg = svg,
|
||||||
|
super.fromSvg() {
|
||||||
|
_initialOptions = Map.from(selectedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
_CustomAvatarMakerController.defaultAvatar() : _initialSvg = '', super() {
|
||||||
|
_initialOptions = Map.from(defaultSelectedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String _initialSvg;
|
||||||
|
late final Map<PropertyCategoryIds, PropertyItem> _initialOptions;
|
||||||
|
List<CustomizedPropertyCategory>? _customPropertyCategories;
|
||||||
|
|
||||||
|
void clearCustomizations() {
|
||||||
|
selectedOptions = Map.from(defaultSelectedOptions);
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CustomizedPropertyCategory> get propertyCategories {
|
||||||
|
var list = _customPropertyCategories;
|
||||||
|
if (list == null) {
|
||||||
|
list = super.propertyCategories.map((category) {
|
||||||
|
return CustomizedPropertyCategory(
|
||||||
|
id: category.id,
|
||||||
|
name: category.name,
|
||||||
|
iconFile: category.iconFile,
|
||||||
|
properties: category.properties,
|
||||||
|
defaultValue: category.defaultValue,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
_customPropertyCategories = list;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CustomizedPropertyCategory> get displayedPropertyCategories {
|
||||||
|
final order = [
|
||||||
|
PropertyCategoryIds.SkinColor,
|
||||||
|
PropertyCategoryIds.EyeType,
|
||||||
|
PropertyCategoryIds.EyebrowType,
|
||||||
|
PropertyCategoryIds.Nose,
|
||||||
|
PropertyCategoryIds.MouthType,
|
||||||
|
PropertyCategoryIds.HairStyle,
|
||||||
|
PropertyCategoryIds.HairColor,
|
||||||
|
PropertyCategoryIds.FacialHairType,
|
||||||
|
PropertyCategoryIds.FacialHairColor,
|
||||||
|
PropertyCategoryIds.OutfitType,
|
||||||
|
PropertyCategoryIds.OutfitColor,
|
||||||
|
PropertyCategoryIds.Accessory,
|
||||||
|
];
|
||||||
|
return (propertyCategories.where((c) => order.contains(c.id)).toList()
|
||||||
|
..sort((a, b) => order.indexOf(a.id).compareTo(order.indexOf(b.id))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RestoredData> performRestore() async {
|
||||||
|
final restoredSvg = _initialSvg.isNotEmpty ? _initialSvg : drawAvatarSVG();
|
||||||
|
return RestoredData(
|
||||||
|
svg: restoredSvg,
|
||||||
|
options: Map.from(_initialOptions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:avatar_maker/avatar_maker.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
@ -10,8 +9,10 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||||
|
|
||||||
class ProfileView extends StatefulWidget {
|
class ProfileView extends StatefulWidget {
|
||||||
|
|
@ -22,9 +23,6 @@ class ProfileView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProfileViewState extends State<ProfileView> {
|
class _ProfileViewState extends State<ProfileView> {
|
||||||
final AvatarMakerController _avatarMakerController =
|
|
||||||
PersistentAvatarMakerController(customizedPropertyCategories: []);
|
|
||||||
|
|
||||||
int twonlyScore = 0;
|
int twonlyScore = 0;
|
||||||
late StreamSubscription<int> twonlyScoreSub;
|
late StreamSubscription<int> twonlyScoreSub;
|
||||||
|
|
||||||
|
|
@ -104,22 +102,24 @@ class _ProfileViewState extends State<ProfileView> {
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
AvatarMakerAvatar(
|
const AvatarIcon(
|
||||||
backgroundColor: Colors.transparent,
|
fontSize: 70,
|
||||||
radius: 80,
|
myAvatar: true,
|
||||||
controller: _avatarMakerController,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Center(
|
Center(
|
||||||
child: SizedBox(
|
child: MyButton(
|
||||||
height: 35,
|
variant: MyButtonVariant.secondaryDense,
|
||||||
child: ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
label: Text(context.lang.settingsProfileCustomizeAvatar),
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await context.push(Routes.settingsProfileModifyAvatar);
|
await context.push(Routes.settingsProfileModifyAvatar);
|
||||||
await _avatarMakerController.performRestore();
|
|
||||||
},
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.edit, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.lang.settingsProfileCustomizeAvatar),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
24
pubspec.lock
24
pubspec.lock
|
|
@ -794,26 +794,26 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40
|
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.0"
|
version: "10.3.1"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_darwin
|
name: flutter_secure_storage_darwin
|
||||||
sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3"
|
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.3.2"
|
||||||
flutter_secure_storage_linux:
|
flutter_secure_storage_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_linux
|
name: flutter_secure_storage_linux
|
||||||
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
|
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.1"
|
||||||
flutter_secure_storage_platform_interface:
|
flutter_secure_storage_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -826,10 +826,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_web
|
name: flutter_secure_storage_web
|
||||||
sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3"
|
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
flutter_secure_storage_windows:
|
flutter_secure_storage_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1303,10 +1303,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1946,10 +1946,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.11"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.2.26+135
|
version: 0.2.29+138
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
@ -94,7 +94,7 @@ dependencies:
|
||||||
archive: ^4.0.7 # 6.5 mio
|
archive: ^4.0.7 # 6.5 mio
|
||||||
file_picker: ^10.3.6 # 2 mio
|
file_picker: ^10.3.6 # 2 mio
|
||||||
get: ^4.7.2 # 740 k
|
get: ^4.7.2 # 740 k
|
||||||
flutter_secure_storage: ^10.0.0 # 1.85 mio
|
flutter_secure_storage: ^10.3.1 # 1.85 mio
|
||||||
permission_handler: ^12.0.0+1 # 2 mio
|
permission_handler: ^12.0.0+1 # 2 mio
|
||||||
|
|
||||||
# Not yet checked
|
# Not yet checked
|
||||||
|
|
@ -206,6 +206,9 @@ flutter_launcher_icons:
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
# config:
|
||||||
|
# enable-swift-package-manager: false
|
||||||
|
|
||||||
# Enable generation of localized Strings from arb files.
|
# Enable generation of localized Strings from arb files.
|
||||||
generate: true
|
generate: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,16 @@ use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion};
|
||||||
|
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::{callback_generator, frb_generated::StreamSink};
|
use crate::{callback_generator, frb_generated::StreamSink};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::Arc;
|
||||||
|
|
||||||
static FLUTTER_CALLBACKS: OnceLock<FlutterCallbacks> = OnceLock::new();
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
tokio::task_local! {
|
||||||
|
pub(crate) static CURRENT_CALLBACK_ID: u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static FLUTTER_CALLBACKS: std::sync::RwLock<Option<HashMap<u32, FlutterCallbacks>>> =
|
||||||
|
std::sync::RwLock::new(None);
|
||||||
|
|
||||||
// This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks
|
// This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks
|
||||||
callback_generator! {
|
callback_generator! {
|
||||||
|
|
@ -39,8 +46,25 @@ callback_generator! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_callbacks() -> Result<&'static FlutterCallbacks> {
|
pub(crate) fn get_callbacks() -> Result<FlutterCallbacks> {
|
||||||
FLUTTER_CALLBACKS
|
let caller_opt = CURRENT_CALLBACK_ID.try_with(|&c| c).ok();
|
||||||
.get()
|
|
||||||
.ok_or(TwonlyError::MissingCallbackInitialization)
|
let lock = FLUTTER_CALLBACKS.read().unwrap();
|
||||||
|
let map = lock.as_ref().ok_or(TwonlyError::MissingCallbackInitialization)?;
|
||||||
|
|
||||||
|
if let Some(id) = caller_opt {
|
||||||
|
if let Some(cb) = map.get(&id) {
|
||||||
|
return Ok(cb.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if not in a scoped tokio task or if the specific callback_id isn't found,
|
||||||
|
// we pick the first available callbacks from the map. This gracefully handles
|
||||||
|
// tracing initialization which happens outside of any scoped task.
|
||||||
|
if let Some((_, cb)) = map.iter().next() {
|
||||||
|
tracing::error!("FlutterCallbacks fallback used: No CURRENT_CALLBACK_ID scope was found, or the ID was missing from the map. Using an arbitrary callback. This may lead to race conditions if multiple isolates are active.");
|
||||||
|
return Ok(cb.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(TwonlyError::MissingCallbackInitialization)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ macro_rules! callback_generator {
|
||||||
) => {
|
) => {
|
||||||
// 1. Generate the Nested Sub-Structs
|
// 1. Generate the Nested Sub-Structs
|
||||||
$(
|
$(
|
||||||
|
#[derive(Clone)]
|
||||||
pub(crate) struct $sub_struct_name {
|
pub(crate) struct $sub_struct_name {
|
||||||
$(
|
$(
|
||||||
pub(crate) $fn_name: Arc<dyn Fn($($input),*) -> DartFnFuture<$output> + Send + Sync + 'static>,
|
pub(crate) $fn_name: Arc<dyn Fn($($input),*) -> DartFnFuture<$output> + Send + Sync + 'static>,
|
||||||
|
|
@ -21,6 +22,7 @@ macro_rules! callback_generator {
|
||||||
)*
|
)*
|
||||||
|
|
||||||
// 2. Generate the Main Container Struct
|
// 2. Generate the Main Container Struct
|
||||||
|
#[derive(Clone)]
|
||||||
pub(crate) struct $struct_name {
|
pub(crate) struct $struct_name {
|
||||||
$(
|
$(
|
||||||
pub(crate) $sub_struct_field: $sub_struct_name,
|
pub(crate) $sub_struct_field: $sub_struct_name,
|
||||||
|
|
@ -30,6 +32,7 @@ macro_rules! callback_generator {
|
||||||
// 3. Generate the Automated Init Function
|
// 3. Generate the Automated Init Function
|
||||||
paste::paste! {
|
paste::paste! {
|
||||||
pub fn init_flutter_callbacks(
|
pub fn init_flutter_callbacks(
|
||||||
|
callback_id: u32,
|
||||||
$(
|
$(
|
||||||
$(
|
$(
|
||||||
// Parameters: sub-struct_field + _ + fn_name
|
// Parameters: sub-struct_field + _ + fn_name
|
||||||
|
|
@ -47,10 +50,11 @@ macro_rules! callback_generator {
|
||||||
)*
|
)*
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the static global strictly named FLUTTER_CALLBACKS
|
let mut lock = FLUTTER_CALLBACKS.write().unwrap();
|
||||||
FLUTTER_CALLBACKS.set(callbacks).unwrap_or_else(|_| {
|
if lock.is_none() {
|
||||||
println!("Callbacks were already initialized!");
|
*lock = Some(std::collections::HashMap::new());
|
||||||
});
|
}
|
||||||
|
lock.as_mut().unwrap().insert(callback_id, callbacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::bridge::callbacks::CURRENT_CALLBACK_ID;
|
||||||
use crate::bridge::get_twonly_flutter;
|
use crate::bridge::get_twonly_flutter;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
|
|
@ -5,11 +6,13 @@ pub struct FlutterUserDiscovery {}
|
||||||
|
|
||||||
impl FlutterUserDiscovery {
|
impl FlutterUserDiscovery {
|
||||||
pub async fn initialize_or_update(
|
pub async fn initialize_or_update(
|
||||||
|
callback_id: u32,
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
public_key: Vec<u8>,
|
public_key: Vec<u8>,
|
||||||
share_promotion: bool,
|
share_promotion: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
CURRENT_CALLBACK_ID.scope(callback_id, async move {
|
||||||
tracing::info!("Rust bridge: initialize_or_update started");
|
tracing::info!("Rust bridge: initialize_or_update started");
|
||||||
let twonly = get_twonly_flutter()?;
|
let twonly = get_twonly_flutter()?;
|
||||||
tracing::info!("Rust bridge: getting user_discovery lock");
|
tracing::info!("Rust bridge: getting user_discovery lock");
|
||||||
|
|
@ -20,63 +23,78 @@ impl FlutterUserDiscovery {
|
||||||
.await;
|
.await;
|
||||||
tracing::info!("Rust bridge: initialize_or_update on protocols finished");
|
tracing::info!("Rust bridge: initialize_or_update on protocols finished");
|
||||||
Ok(res?)
|
Ok(res?)
|
||||||
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_current_version() -> Result<Vec<u8>> {
|
pub async fn get_current_version(callback_id: u32) -> Result<Vec<u8>> {
|
||||||
|
CURRENT_CALLBACK_ID.scope(callback_id, async move {
|
||||||
Ok(get_twonly_flutter()?
|
Ok(get_twonly_flutter()?
|
||||||
.user_discovery
|
.user_discovery
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.get_current_version()
|
.get_current_version()
|
||||||
.await?)
|
.await?)
|
||||||
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_new_messages(
|
pub async fn get_new_messages(
|
||||||
|
callback_id: u32,
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
received_version: &[u8],
|
received_version: &[u8],
|
||||||
) -> Result<Vec<Vec<u8>>> {
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
|
CURRENT_CALLBACK_ID.scope(callback_id, async move {
|
||||||
Ok(get_twonly_flutter()?
|
Ok(get_twonly_flutter()?
|
||||||
.user_discovery
|
.user_discovery
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.get_new_messages(contact_id, received_version)
|
.get_new_messages(contact_id, received_version)
|
||||||
.await?)
|
.await?)
|
||||||
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn should_request_new_messages(
|
pub async fn should_request_new_messages(
|
||||||
|
callback_id: u32,
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
version: &[u8],
|
version: &[u8],
|
||||||
) -> Result<Option<Vec<u8>>> {
|
) -> Result<Option<Vec<u8>>> {
|
||||||
|
CURRENT_CALLBACK_ID.scope(callback_id, async move {
|
||||||
Ok(get_twonly_flutter()?
|
Ok(get_twonly_flutter()?
|
||||||
.user_discovery
|
.user_discovery
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.should_request_new_messages(contact_id, version)
|
.should_request_new_messages(contact_id, version)
|
||||||
.await?)
|
.await?)
|
||||||
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_new_messages(
|
pub async fn handle_new_messages(
|
||||||
|
callback_id: u32,
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
public_key_verified_timestamp: Option<i64>,
|
public_key_verified_timestamp: Option<i64>,
|
||||||
messages: Vec<Vec<u8>>,
|
messages: Vec<Vec<u8>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
CURRENT_CALLBACK_ID.scope(callback_id, async move {
|
||||||
Ok(get_twonly_flutter()?
|
Ok(get_twonly_flutter()?
|
||||||
.user_discovery
|
.user_discovery
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.handle_new_messages(contact_id, public_key_verified_timestamp, messages)
|
.handle_new_messages(contact_id, public_key_verified_timestamp, messages)
|
||||||
.await?)
|
.await?)
|
||||||
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_verification_state_for_user(
|
pub async fn update_verification_state_for_user(
|
||||||
|
callback_id: u32,
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
public_key_verified_timestamp: Option<i64>,
|
public_key_verified_timestamp: Option<i64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
CURRENT_CALLBACK_ID.scope(callback_id, async move {
|
||||||
Ok(get_twonly_flutter()?
|
Ok(get_twonly_flutter()?
|
||||||
.user_discovery
|
.user_discovery
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.update_verification_state_for_user(contact_id, public_key_verified_timestamp)
|
.update_verification_state_for_user(contact_id, public_key_verified_timestamp)
|
||||||
.await?)
|
.await?)
|
||||||
|
}).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,9 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_curr
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_get_current_version", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_get_current_version", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
deserializer.end(); move |context| async move {
|
let api_callback_id = <u32>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
||||||
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::get_current_version().await?; Ok(output_ok)
|
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::get_current_version(api_callback_id).await?; Ok(output_ok)
|
||||||
})().await)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
@ -70,10 +70,11 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_new_
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_get_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_get_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_callback_id = <u32>::sse_decode(&mut deserializer);
|
||||||
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
||||||
let api_received_version = <Vec<u8>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
let api_received_version = <Vec<u8>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
||||||
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::get_new_messages(api_contact_id, &api_received_version).await?; Ok(output_ok)
|
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::get_new_messages(api_callback_id, api_contact_id, &api_received_version).await?; Ok(output_ok)
|
||||||
})().await)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
@ -86,11 +87,12 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_n
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_handle_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_handle_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_callback_id = <u32>::sse_decode(&mut deserializer);
|
||||||
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
||||||
let api_public_key_verified_timestamp = <Option<i64>>::sse_decode(&mut deserializer);
|
let api_public_key_verified_timestamp = <Option<i64>>::sse_decode(&mut deserializer);
|
||||||
let api_messages = <Vec<Vec<u8>>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
let api_messages = <Vec<Vec<u8>>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
||||||
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::handle_new_messages(api_contact_id, api_public_key_verified_timestamp, api_messages).await?; Ok(output_ok)
|
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::handle_new_messages(api_callback_id, api_contact_id, api_public_key_verified_timestamp, api_messages).await?; Ok(output_ok)
|
||||||
})().await)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
@ -103,12 +105,13 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initiali
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_initialize_or_update", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_initialize_or_update", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_callback_id = <u32>::sse_decode(&mut deserializer);
|
||||||
let api_threshold = <u8>::sse_decode(&mut deserializer);
|
let api_threshold = <u8>::sse_decode(&mut deserializer);
|
||||||
let api_user_id = <i64>::sse_decode(&mut deserializer);
|
let api_user_id = <i64>::sse_decode(&mut deserializer);
|
||||||
let api_public_key = <Vec<u8>>::sse_decode(&mut deserializer);
|
let api_public_key = <Vec<u8>>::sse_decode(&mut deserializer);
|
||||||
let api_share_promotion = <bool>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
let api_share_promotion = <bool>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
||||||
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::initialize_or_update(api_threshold, api_user_id, api_public_key, api_share_promotion).await?; Ok(output_ok)
|
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::initialize_or_update(api_callback_id, api_threshold, api_user_id, api_public_key, api_share_promotion).await?; Ok(output_ok)
|
||||||
})().await)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
@ -121,10 +124,11 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_should_r
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_should_request_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_should_request_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_callback_id = <u32>::sse_decode(&mut deserializer);
|
||||||
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
||||||
let api_version = <Vec<u8>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
let api_version = <Vec<u8>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
||||||
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::should_request_new_messages(api_contact_id, &api_version).await?; Ok(output_ok)
|
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::should_request_new_messages(api_callback_id, api_contact_id, &api_version).await?; Ok(output_ok)
|
||||||
})().await)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
@ -137,10 +141,11 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_v
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_update_verification_state_for_user", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_update_verification_state_for_user", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_callback_id = <u32>::sse_decode(&mut deserializer);
|
||||||
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
||||||
let api_public_key_verified_timestamp = <Option<i64>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
let api_public_key_verified_timestamp = <Option<i64>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
||||||
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::update_verification_state_for_user(api_contact_id, api_public_key_verified_timestamp).await?; Ok(output_ok)
|
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::update_verification_state_for_user(api_callback_id, api_contact_id, api_public_key_verified_timestamp).await?; Ok(output_ok)
|
||||||
})().await)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +158,7 @@ fn wire__crate__bridge__callbacks__init_flutter_callbacks_impl(
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "init_flutter_callbacks", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "init_flutter_callbacks", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_callback_id = <u32>::sse_decode(&mut deserializer);
|
||||||
let api_logging_get_stream_sink = decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
let api_logging_get_stream_sink = decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
||||||
let api_user_discovery_sign_data = decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
let api_user_discovery_sign_data = decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
||||||
let api_user_discovery_verify_signature = decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
let api_user_discovery_verify_signature = decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
||||||
|
|
@ -169,7 +175,7 @@ let api_user_discovery_set_contact_version = decode_DartFn_Inputs_i_64_list_prim
|
||||||
let api_user_discovery_push_new_user_relation = decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
let api_user_discovery_push_new_user_relation = decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
|
||||||
let api_user_discovery_get_contact_promotion = decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));deserializer.end(); move |context| {
|
let api_user_discovery_get_contact_promotion = decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));deserializer.end(); move |context| {
|
||||||
transform_result_sse::<_, ()>((move || {
|
transform_result_sse::<_, ()>((move || {
|
||||||
let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_logging_get_stream_sink, api_user_discovery_sign_data, api_user_discovery_verify_signature, api_user_discovery_verify_stored_pubkey, api_user_discovery_set_shares, api_user_discovery_get_share_for_contact, api_user_discovery_push_own_promotion_and_clear_old_version, api_user_discovery_get_own_promotions_after_version, api_user_discovery_store_other_promotion, api_user_discovery_get_other_promotions_by_public_id, api_user_discovery_get_announced_user_by_public_id, api_user_discovery_get_contact_version, api_user_discovery_set_contact_version, api_user_discovery_push_new_user_relation, api_user_discovery_get_contact_promotion); })?; Ok(output_ok)
|
let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_callback_id, api_logging_get_stream_sink, api_user_discovery_sign_data, api_user_discovery_verify_signature, api_user_discovery_verify_stored_pubkey, api_user_discovery_set_shares, api_user_discovery_get_share_for_contact, api_user_discovery_push_own_promotion_and_clear_old_version, api_user_discovery_get_own_promotions_after_version, api_user_discovery_store_other_promotion, api_user_discovery_get_other_promotions_by_public_id, api_user_discovery_get_announced_user_by_public_id, api_user_discovery_get_contact_version, api_user_discovery_set_contact_version, api_user_discovery_push_new_user_relation, api_user_discovery_get_contact_promotion); })?; Ok(output_ok)
|
||||||
})())
|
})())
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
test.jpg
BIN
test.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 620 B |
118
test/services/messages_purge_test.dart
Normal file
118
test/services/messages_purge_test.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await locator.reset();
|
||||||
|
locator
|
||||||
|
..registerSingleton<TwonlyDB>(
|
||||||
|
TwonlyDB.forTesting(
|
||||||
|
DatabaseConnection(
|
||||||
|
NativeDatabase.memory(),
|
||||||
|
closeStreamsSynchronously: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..registerSingleton<UserService>(UserService());
|
||||||
|
|
||||||
|
userService.currentUser = UserData(
|
||||||
|
userId: 1,
|
||||||
|
username: 'test_user',
|
||||||
|
displayName: 'Test User',
|
||||||
|
subscriptionPlan: 'Free',
|
||||||
|
currentSetupPage: null,
|
||||||
|
appVersion: 100,
|
||||||
|
);
|
||||||
|
userService.isUserCreated = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await twonlyDB.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('purgeMessageTable preserves unopened messages and deletes expired ones', () async {
|
||||||
|
final now = clock.now();
|
||||||
|
const retentionMs = 7200000; // 2 hours
|
||||||
|
final deletionLimit = now.subtract(const Duration(milliseconds: retentionMs));
|
||||||
|
|
||||||
|
// 1. Insert a group with 2 hour retention policy
|
||||||
|
await twonlyDB.groupsDao.createNewGroup(
|
||||||
|
GroupsCompanion.insert(
|
||||||
|
groupId: 'test_group',
|
||||||
|
groupName: 'Test Group',
|
||||||
|
deleteMessagesAfterMilliseconds: const Value(retentionMs),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Insert test messages:
|
||||||
|
// Msg A: Unopened (openedByAll is null)
|
||||||
|
await twonlyDB.messagesDao.insertMessage(
|
||||||
|
MessagesCompanion.insert(
|
||||||
|
messageId: 'msg_a_unopened',
|
||||||
|
groupId: 'test_group',
|
||||||
|
type: 'text',
|
||||||
|
createdAt: Value(deletionLimit.subtract(const Duration(minutes: 5))), // older than deletion threshold
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Msg B: Opened long ago (openedByAll is older than deletion threshold)
|
||||||
|
await twonlyDB.messagesDao.insertMessage(
|
||||||
|
MessagesCompanion.insert(
|
||||||
|
messageId: 'msg_b_opened_expired',
|
||||||
|
groupId: 'test_group',
|
||||||
|
type: 'text',
|
||||||
|
openedByAll: Value(deletionLimit.subtract(const Duration(minutes: 5))),
|
||||||
|
createdAt: Value(deletionLimit.subtract(const Duration(minutes: 30))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Msg C: Opened recently (openedByAll is newer than deletion threshold)
|
||||||
|
await twonlyDB.messagesDao.insertMessage(
|
||||||
|
MessagesCompanion.insert(
|
||||||
|
messageId: 'msg_c_opened_recent',
|
||||||
|
groupId: 'test_group',
|
||||||
|
type: 'text',
|
||||||
|
openedByAll: Value(deletionLimit.add(const Duration(minutes: 5))),
|
||||||
|
createdAt: Value(deletionLimit.subtract(const Duration(minutes: 10))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Msg D: Deleted from sender, older than threshold
|
||||||
|
await twonlyDB.messagesDao.insertMessage(
|
||||||
|
MessagesCompanion.insert(
|
||||||
|
messageId: 'msg_d_sender_deleted_expired',
|
||||||
|
groupId: 'test_group',
|
||||||
|
type: 'text',
|
||||||
|
isDeletedFromSender: const Value(true),
|
||||||
|
createdAt: Value(deletionLimit.subtract(const Duration(minutes: 5))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run purge
|
||||||
|
await twonlyDB.messagesDao.purgeMessageTable();
|
||||||
|
|
||||||
|
// Verify database state
|
||||||
|
final allMessages = await twonlyDB.select(twonlyDB.messages).get();
|
||||||
|
final remainingIds = allMessages.map((m) => m.messageId).toList();
|
||||||
|
|
||||||
|
// msg_a_unopened should be preserved because it was never opened (openedByAll was null)
|
||||||
|
expect(remainingIds.contains('msg_a_unopened'), isTrue);
|
||||||
|
|
||||||
|
// msg_b_opened_expired should be deleted because openedByAll < deletionLimit
|
||||||
|
expect(remainingIds.contains('msg_b_opened_expired'), isFalse);
|
||||||
|
|
||||||
|
// msg_c_opened_recent should be preserved because openedByAll >= deletionLimit
|
||||||
|
expect(remainingIds.contains('msg_c_opened_recent'), isTrue);
|
||||||
|
|
||||||
|
// msg_d_sender_deleted_expired should be deleted because isDeletedFromSender is true and createdAt < deletionLimit
|
||||||
|
expect(remainingIds.contains('msg_d_sender_deleted_expired'), isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue