mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 11:24:08 +00:00
Compare commits
No commits in common. "main" and "v0.5.1" have entirely different histories.
70 changed files with 837 additions and 1889 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -56,4 +56,3 @@ devtools_options.yaml
|
||||||
rust/target
|
rust/target
|
||||||
rust_dependencies/target
|
rust_dependencies/target
|
||||||
fastlane/repo/status/running.json
|
fastlane/repo/status/running.json
|
||||||
.cache/
|
|
||||||
228
CHANGELOG.md
228
CHANGELOG.md
|
|
@ -1,18 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.5.2
|
|
||||||
|
|
||||||
- Hotfix: Fixed an infinite loop involving delivery receipts
|
|
||||||
|
|
||||||
## 0.5.1
|
|
||||||
|
|
||||||
- Improve: Show delivery and read receipt indicators for text messages
|
|
||||||
- Improve: Backup screen clearer and easier to understand
|
|
||||||
- Improve: Show username above messages in group chats
|
|
||||||
- Fix: Background audio correctly pauses and resumes when viewing videos
|
|
||||||
- Fix: Sometimes old messages were being received as duplicates
|
|
||||||
- Fix: Multiple smaller bug fixes
|
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
- New: Update to Signal's new PQC-ready key agreement PQXDH
|
- New: Update to Signal's new PQC-ready key agreement PQXDH
|
||||||
|
|
@ -62,8 +49,8 @@
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|
||||||
- Improve: Design of some UI components
|
- Improved: Design of some UI components
|
||||||
- Improve: Memories viewer shows state for batch operations and has improved performance
|
- Improved: Memories viewer shows state for batch operations and has improved performance
|
||||||
- Fix: Issue with background notifications on Android
|
- Fix: Issue with background notifications on Android
|
||||||
- Fix: Changed minimum threshold for the user discovery to 3
|
- Fix: Changed minimum threshold for the user discovery to 3
|
||||||
- Fix: Multiple UI issues
|
- Fix: Multiple UI issues
|
||||||
|
|
@ -72,24 +59,24 @@
|
||||||
## 0.2.26
|
## 0.2.26
|
||||||
|
|
||||||
- New: Import images from the gallery
|
- New: Import images from the gallery
|
||||||
- Improve: Media files are now stored in the dedicated "twonly" album
|
- Improved: Media files are now stored in the dedicated "twonly" album
|
||||||
- Improve: UI components adapt to native styling (iOS/Android)
|
- Improved: UI components adapt to native styling (iOS/Android)
|
||||||
- Fix: Migration issue that resulted in a corrupted backup mechanism
|
- Fix: Migration issue that resulted in a corrupted backup mechanism
|
||||||
- Fix: Database issues causing messages to be lost or the database to be corrupted
|
- Fix: Database issues causing messages to be lost or the database to be corrupted
|
||||||
- Fix: Permission view did not disappear after they were granted
|
- Fix: Permission view did not disappear after they were granted
|
||||||
|
|
||||||
## 0.2.23
|
## 0.2.23
|
||||||
|
|
||||||
- Improve: Smaller UI changes
|
- Improved: Smaller UI changes
|
||||||
- Fix: Some messages were not marked as opened.
|
- Fix: Some messages were not marked as opened.
|
||||||
|
|
||||||
## 0.2.20
|
## 0.2.20
|
||||||
|
|
||||||
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
||||||
- New: Adds security profiles.
|
- New: Adds security profiles.
|
||||||
- Improve: Onboarding flow for new users.
|
- Improved: Onboarding flow for new users.
|
||||||
- Improve: Flame restore experience.
|
- Improved: Flame restore experience.
|
||||||
- Improve: The blue verification checkmark now displays the total number of verifications.
|
- Improved: The blue verification checkmark now displays the total number of verifications.
|
||||||
- Fix: Issue with receiving messages when user closed app while decrypting
|
- Fix: Issue with receiving messages when user closed app while decrypting
|
||||||
- Fix: Background message fetching reliability.
|
- Fix: Background message fetching reliability.
|
||||||
- Fix: Issue with focus changing when taking a picture
|
- Fix: Issue with focus changing when taking a picture
|
||||||
|
|
@ -108,22 +95,22 @@
|
||||||
|
|
||||||
- New: Tutorial on how to use zoom.
|
- New: Tutorial on how to use zoom.
|
||||||
- New: Manage storage view.
|
- New: Manage storage view.
|
||||||
- Improve: Media thumbnails for faster loading.
|
- Improved: Media thumbnails for faster loading.
|
||||||
- Fix: Some messages were not marked as opened.
|
- Fix: Some messages were not marked as opened.
|
||||||
|
|
||||||
## 0.2.12
|
## 0.2.12
|
||||||
|
|
||||||
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
|
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
|
||||||
- Improve: Memories viewer redesigned with smoother animations and new quick-action controls.
|
- Improved: Memories viewer redesigned with smoother animations and new quick-action controls.
|
||||||
- Fix: Reliability of receiving media files.
|
- Fix: Reliability of receiving media files.
|
||||||
|
|
||||||
## 0.2.11
|
## 0.2.11
|
||||||
|
|
||||||
- New: Create custom shortcuts to quickly share images with pre-selected groups
|
- New: Create custom shortcuts to quickly share images with pre-selected groups
|
||||||
- New: Seamless recovery for iOS reinstallations
|
- New: Seamless recovery for iOS reinstallations
|
||||||
- Improve: Redesigned snackbar notifications
|
- Improved: Redesigned snackbar notifications
|
||||||
- Improve: New backup mechanism to allow larger backup files
|
- Improved: New backup mechanism to allow larger backup files
|
||||||
- Improve: Move keys into a centralized Rust-owned structure stored in secure storage
|
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
|
||||||
- Fix: Messages occasionally not received until app restart
|
- Fix: Messages occasionally not received until app restart
|
||||||
- Fix: Multiple smaller issues
|
- Fix: Multiple smaller issues
|
||||||
|
|
||||||
|
|
@ -133,7 +120,7 @@
|
||||||
|
|
||||||
## 0.2.9
|
## 0.2.9
|
||||||
|
|
||||||
- Improve: Make contact avatars clickable
|
- Improved: Make contact avatars clickable
|
||||||
- Fix: Messages occasionally not received until app restart
|
- Fix: Messages occasionally not received until app restart
|
||||||
- Fix: Complete setup would sometimes get stuck
|
- Fix: Complete setup would sometimes get stuck
|
||||||
|
|
||||||
|
|
@ -146,21 +133,21 @@
|
||||||
- New: Feature to find friends without a phone number
|
- New: Feature to find friends without a phone number
|
||||||
- New: The verification state is now transferred to the scanned user
|
- New: The verification state is now transferred to the scanned user
|
||||||
- New: Registration setup to configure the most important configurations
|
- New: Registration setup to configure the most important configurations
|
||||||
- Improve: Show ⌛ instead of the flame icon when it is about to expire
|
- Improved: Show ⌛ instead of the flame icon when it is about to expire
|
||||||
- Improve: FAQ is now in the app rather than opening in the browser
|
- Improved: FAQ is now in the app rather than opening in the browser
|
||||||
- Improve: Videos can now be paused
|
- Improved: Videos can now be paused
|
||||||
- Improve: Lock to record hands-free
|
- Improved: Lock to record hands-free
|
||||||
- Fix: Many smaller issues
|
- Fix: Many smaller issues
|
||||||
|
|
||||||
## 0.1.8
|
## 0.1.8
|
||||||
|
|
||||||
- Improve: Typos and grammar issues thanks to @AlbertUnruh
|
- Improved: Typos and grammar issues thanks to @AlbertUnruh
|
||||||
- Fix: App becomes unresponsive when clicking notifications.
|
- Fix: App becomes unresponsive when clicking notifications.
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
- Improve: Show input indicator in the chat overview as well
|
- Improved: Show input indicator in the chat overview as well
|
||||||
- Improve: Username change error handling
|
- Improved: Username change error handling
|
||||||
- Fix: Phantom push notification
|
- Fix: Phantom push notification
|
||||||
- Fix: Start in chat, if configured
|
- Fix: Start in chat, if configured
|
||||||
- Fix: Smaller UI fixes
|
- Fix: Smaller UI fixes
|
||||||
|
|
@ -212,7 +199,7 @@
|
||||||
|
|
||||||
## 0.0.96
|
## 0.0.96
|
||||||
|
|
||||||
- New: Show link in chat if the saved media file contains one
|
- Feature: Show link in chat if the saved media file contains one
|
||||||
- Improve: Verification badge for groups
|
- Improve: Verification badge for groups
|
||||||
- Improve: Huge reduction in app size
|
- Improve: Huge reduction in app size
|
||||||
- Fix: Crash on older devices when compressing a video
|
- Fix: Crash on older devices when compressing a video
|
||||||
|
|
@ -220,7 +207,7 @@
|
||||||
|
|
||||||
## 0.0.93
|
## 0.0.93
|
||||||
|
|
||||||
- New: Verification checkmark for friends
|
- Feature: Verification checkmark for friends
|
||||||
- Fix: Added contacts in contact sharing that were not clickable.
|
- Fix: Added contacts in contact sharing that were not clickable.
|
||||||
- Fix: Open chat after the image expires in case a draft message exists
|
- Fix: Open chat after the image expires in case a draft message exists
|
||||||
- Fix: Restore flames as a plus user
|
- Fix: Restore flames as a plus user
|
||||||
|
|
@ -233,138 +220,139 @@
|
||||||
|
|
||||||
## 0.0.92
|
## 0.0.92
|
||||||
|
|
||||||
- New: The option to share contacts
|
- Adds the option to share contacts
|
||||||
- New: Option to zoom in received images / videos
|
- Adds option to zoom in received images / videos
|
||||||
- Fix: Issue with "reuploaded requested" not working
|
- Fixes issue with "reuploaded requested" not working
|
||||||
- Fix: Race condition while writing to the log file
|
- Fixes race condition while writing to the log file
|
||||||
|
|
||||||
## 0.0.91
|
## 0.0.91
|
||||||
|
|
||||||
- Fix: Link preview on iOS
|
- Fixes link preview on iOS
|
||||||
- Fix: Sharing images from other apps on iOS
|
- Fixes sharing images from other apps on iOS
|
||||||
|
|
||||||
## 0.0.90
|
## 0.0.90
|
||||||
|
|
||||||
- Fix: Issue that media files were not reuploaded
|
- Fixes issue that media files where not reuploaded
|
||||||
- Fix: iOS zooming issue when switching between .5 and x1
|
- Fixes iOS zooming issue when switching between .5 and x1
|
||||||
- Fix: Biometric auth bypass when opening a twonly/reopen send image
|
- Fixes biometric auth bypass when opening a twonly/reopen send image
|
||||||
- Fix: That media files could not be downloaded in case the contact deleted his account
|
- Fixes that media files could not be downloaded in case the contact deleted his account
|
||||||
- Fix: Database issue in case twonly is opened multiple times
|
- Fixes database issue in case twonly is opened multiple times
|
||||||
- Fix: Typos in translation
|
- Fixes typos in translation
|
||||||
|
|
||||||
## 0.0.87
|
## 0.0.87
|
||||||
|
|
||||||
- New: Link preview to shared links
|
- Adds link preview to shared links
|
||||||
- New: Option to manual focus in the camera
|
- Adds option to manual focus in the camera
|
||||||
- New: Support to switch between front and back cameras during video recording
|
- Adds support to switch between front and back cameras during video recording
|
||||||
- New: Basic face filters
|
- Adds basic face filters
|
||||||
- Improve: Image editor, like emojis or text under a drawing can be moved
|
- Improves image editor, like emojis or text under a drawing can be moved
|
||||||
- Improve: Speed after taking a picture
|
- Improves speed after taking a picture
|
||||||
- Fix: Issue with emojis disappearing in the image editor
|
- Fixes issue with emojis disappearing in the image editor
|
||||||
|
|
||||||
## 0.0.86
|
## 0.0.86
|
||||||
|
|
||||||
- New: Allows to reopen send images (if send without time limit or enabled auth)
|
- Allows to reopen send images (if send without time limit or enabled auth)
|
||||||
- New: Support for front camera zoom
|
- Added support for front camera zoom
|
||||||
- Fix: Several bug fixes
|
- Several bug fixes
|
||||||
|
|
||||||
## 0.0.83
|
## 0.0.83
|
||||||
|
|
||||||
- Improve: View of the diagnostic log
|
- Improved view of the diagnostic log
|
||||||
- Fix: Several bug fixes
|
- Several bug fixes
|
||||||
|
|
||||||
## 0.0.82
|
## 0.0.82
|
||||||
|
|
||||||
- New: An option in the settings to automatically save all sent images
|
- Added an option in the settings to automatically save all sent images
|
||||||
- New: Hides duplicate images in the memory
|
- Hides duplicate images in the memory
|
||||||
- Improve: Several other minor improvements
|
- Fixes a bug where messages were not being received
|
||||||
- Fix: A bug where messages were not being received
|
- Several other minor improvements
|
||||||
|
|
||||||
## 0.0.81
|
## 0.0.81
|
||||||
|
|
||||||
- Fix: The issue where black/blank images were sometimes received
|
- Fixes the issue where black/blank images were sometimes received
|
||||||
- Fix: An issue in the image editor
|
- Fixes an issue in the image editor
|
||||||
|
|
||||||
## 0.0.80
|
## 0.0.80
|
||||||
|
|
||||||
- New: Share images/videos directly from other applications
|
- Share images/videos directly from other applications
|
||||||
- New: More customization options in the appearance settings
|
- More customization options in the appearance settings
|
||||||
- Improve: UI for changing the display time of images
|
- Improved UI for changing the display time of images
|
||||||
- Improve: Several minor UI improvements
|
- Several minor UI improvements
|
||||||
- Fix: Several bug fixes
|
- Several bug fixes
|
||||||
|
|
||||||
## 0.0.74
|
## 0.0.74
|
||||||
|
|
||||||
- Improve: Uploading speed
|
- Improving uploading speed
|
||||||
- Fix: Issue with ffmpeg for android
|
- Fixing issue with ffmpeg for android
|
||||||
|
|
||||||
## 0.0.73
|
## 0.0.73
|
||||||
|
|
||||||
- New: Integrated QR code scanner in the main camera
|
- Integrated QR code scanner in the main camera
|
||||||
- New: Profile share page
|
- New profile share page
|
||||||
- New: Workflow for checking the security number
|
- New workflow for checking the security number
|
||||||
- Improve: User interface for creating voice messages
|
- Improved user interface for creating voice messages
|
||||||
|
|
||||||
## 0.0.69
|
## 0.0.69
|
||||||
|
|
||||||
- New: Option to export and import memories
|
- Option to export and import memories
|
||||||
- New: iOS support for ultra-wide-angle camera
|
- iOS support for ultra-wide-angle camera
|
||||||
- New: Support Android Monochrome Icon
|
- Support Android Monochrome Icon
|
||||||
- Fix: Multiple layout issues fixed
|
- Multiple layout issues fixed
|
||||||
- Fix: Multiple bug fixes
|
- Multiple bug fixes
|
||||||
|
|
||||||
## 0.0.67
|
## 0.0.67
|
||||||
|
|
||||||
- New: Crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
|
- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
|
||||||
- Fix: Bug when saving images to the gallery
|
- Fixes bug when saving images to the gallery
|
||||||
- Fix: Multiple layout issues fixed
|
- Multiple layout issues fixed
|
||||||
- Fix: Multiple bug fixes
|
- Multiple bug fixes
|
||||||
|
|
||||||
## 0.0.62
|
## 0.0.62
|
||||||
|
|
||||||
- New: Support for groups with multiple administrators
|
- Support for groups with multiple administrators
|
||||||
- New: Edit and delete messages
|
- Edit and delete messages
|
||||||
- New: Create images using volume buttons
|
- Create images using volume buttons
|
||||||
- New: Removing audio after recording is possible
|
- New and improved emoji picker
|
||||||
- New: Edited image is now embedded into the video
|
- Removing audio after recording is possible
|
||||||
- New: Video max length increased to 60 seconds
|
- Edited image is now embedded into the video
|
||||||
- New: Context menu and other UI enhancements
|
- Video max length increased to 60 seconds
|
||||||
- New: Client-to-client protocol migrated to Protocol Buffers (Protobuf)
|
- Switched to FFmpeg for improved video compression
|
||||||
- New: Database identifiers converted to UUIDs and the database schema completely redesigned
|
- New context menu and other UI enhancements
|
||||||
- Improve: Emoji picker
|
- Client-to-client protocol migrated to Protocol Buffers (Protobuf)
|
||||||
- Improve: Switched to FFmpeg for improved video compression
|
- Database identifiers converted to UUIDs and the database schema completely redesigned
|
||||||
- Improve: Reliability of client-to-client messaging
|
- Improved reliability of client-to-client messaging
|
||||||
- Fix: Multiple bug fixes
|
- Multiple bug fixes
|
||||||
|
|
||||||
## 0.0.61
|
## 0.0.61
|
||||||
|
|
||||||
- New: Image editor when changing colors
|
- Improving image editor when changing colors
|
||||||
- New: Dependency and Flutter upgrade
|
- Fixing message decryption error
|
||||||
- Fix: Message decryption error
|
- Fixing issue with user deletion
|
||||||
- Fix: Issue with user deletion
|
- Fixing issue with flame counter sync
|
||||||
- Fix: Issue with flame counter sync
|
- Dependency and Flutter upgrade
|
||||||
|
|
||||||
## 0.0.60
|
## 0.0.60
|
||||||
|
|
||||||
- New: Display your own avatar in the title bar of the chat list.
|
- Improved logging to debug the 'Tap to load' issue.
|
||||||
- New: Created a default avatar image in case none was set.
|
- Display your own avatar in the title bar of the chat list.
|
||||||
- New: Flutter SDK and dependencies upgraded.
|
- Created a default avatar image in case none was set.
|
||||||
- Improve: Logging to debug the 'Tap to load' issue.
|
- Improved UI handling when requesting microphone access for the first time.
|
||||||
- Improve: UI handling when requesting microphone access for the first time.
|
- Flutter SDK and dependencies upgraded.
|
||||||
- Fix: Multiple bug fixes.
|
- Multiple bug fixes.
|
||||||
|
|
||||||
## 0.0.59
|
## 0.0.59
|
||||||
|
|
||||||
- New: Location Filter are now stored as WebP instead of PNG
|
- Fixing media download error
|
||||||
- Fix: Media download error
|
- Fixing issue with video recording
|
||||||
- Fix: Issue with video recording
|
- Location Filter are now stored as WebP instead of PNG
|
||||||
|
|
||||||
## 0.0.58
|
## 0.0.58
|
||||||
|
|
||||||
- New: iOS gestures to close images
|
- twonly now has a free plan and is now financed by donations and an optional subscription with more features (coming soon)
|
||||||
- New: Onboarding screens updated and registration view simplified
|
- iOS gestures to close images
|
||||||
- New: The sender is displayed in the top right corner when a media file is opened
|
- Improved chat messages view, including better citation view and display times
|
||||||
- New: Images are now stored as WebP to save storage
|
- Onboarding screens updated and registration view simplified
|
||||||
- New: Button to report users
|
- The sender is displayed in the top right corner when a media file is opened
|
||||||
- Improve: Chat messages view, including better citation view and display times
|
- Images are now stored as WebP to save storage
|
||||||
- Fix: Multiple bug fixes
|
- Button to report users
|
||||||
|
- Multiple bug fixes
|
||||||
13
README.md
13
README.md
|
|
@ -32,7 +32,7 @@ If you decide to give twonly a try, please keep in mind that it is still in its
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Offer a Snapchat™ like experience
|
- Offer a Snapchat™ like experience
|
||||||
- End-to-End encryption using the [Signal Protocol](https://signal.org/docs/specifications/pqxdh/)
|
- End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll)
|
||||||
- Open Source and can be downloaded directly from GitHub
|
- Open Source and can be downloaded directly from GitHub
|
||||||
- No email or phone number required to register
|
- No email or phone number required to register
|
||||||
- The backend is hosted exclusively in Europe
|
- The backend is hosted exclusively in Europe
|
||||||
|
|
@ -40,12 +40,19 @@ If you decide to give twonly a try, please keep in mind that it is still in its
|
||||||
- [User Discovery](https://twonly.eu/en/blog/2026-mutual-friends.html) without a phone number
|
- [User Discovery](https://twonly.eu/en/blog/2026-mutual-friends.html) without a phone number
|
||||||
- E2EE cloud backup of memories
|
- E2EE cloud backup of memories
|
||||||
|
|
||||||
## Next on the bucket list
|
## Roadmap
|
||||||
|
|
||||||
|
### Currently
|
||||||
|
|
||||||
|
- Implementation of features so that Snapchat can actually be replaced
|
||||||
- Importing memories from Snapchat
|
- Importing memories from Snapchat
|
||||||
|
|
||||||
|
### Next on the bucket list
|
||||||
|
|
||||||
- For Android: Support for [UnifiedPush] (https://unifiedpush.org/)
|
- For Android: Support for [UnifiedPush] (https://unifiedpush.org/)
|
||||||
- For Android: Reproducible builds
|
- For Android: Reproducible builds
|
||||||
- Implementation of [Sealed Sender](https://signal.org/blog/sealed-sender/) (or a similar protocol) to minimize metadata
|
- Implementation of [Sealed Sender](https://signal.org/blog/sealed-sender/) (or a similar protocol) to minimize metadata
|
||||||
|
- Switch from the Signal protocol to [MLS](https://github.com/openmls/openmls) for post-quantum crypto support
|
||||||
- Decentralize the server so that anyone can run their own server
|
- Decentralize the server so that anyone can run their own server
|
||||||
|
|
||||||
## Security Issues
|
## Security Issues
|
||||||
|
|
@ -56,7 +63,7 @@ guarantee a bounty currently :/
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|
||||||
Currently there are still some core features and rewrites open I want to do involving a huge rewrite in Rust. Because of this, contributions are currently not wanted. You can still view the code (if you find any security issues please contact me!). Also, issues are currently closed. If you find a bug, please use the in-app option as there you can upload your debug log which helps a lot.
|
Currently there are still some core features and rewrites open I want to do like the switch to the MLS protocol involving a huge rewrite in Rust. Because of this, contributions are currently not wanted. You can still view the code (if you find any security issues please contact me!). Also, issues are currently closed. If you find a bug, please use the in-app option as there you can upload your debug log which helps a lot.
|
||||||
|
|
||||||
## Signing Keys
|
## Signing Keys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,6 @@ analyzer:
|
||||||
- "**.arb"
|
- "**.arb"
|
||||||
- "test/drift/**"
|
- "test/drift/**"
|
||||||
- "**.g.dart"
|
- "**.g.dart"
|
||||||
- build/**
|
|
||||||
- android/**
|
|
||||||
- ios/**
|
|
||||||
- web/**
|
|
||||||
- windows/**
|
|
||||||
- macos/**
|
|
||||||
- linux/**
|
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ android {
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
manifestPlaceholders = [appName: "twonly"]
|
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {
|
release {
|
||||||
|
|
@ -54,7 +53,6 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix ".testing"
|
applicationIdSuffix ".testing"
|
||||||
manifestPlaceholders = [appName: "twonly [dev]"]
|
|
||||||
}
|
}
|
||||||
// profile {
|
// profile {
|
||||||
// applicationIdSuffix ".STOP"
|
// applicationIdSuffix ".STOP"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||||
<application
|
<application
|
||||||
android:label="${appName}"
|
android:label="twonly"
|
||||||
android:name=".MyApplication"
|
android:name=".MyApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="false"
|
android:fullBackupContent="false"
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit b874b9cc309822d90b940fdfbc8fd1ad3afc2450
|
Subproject commit 32882a31904a0c1ffd38974bf5158ceee0f3ecb7
|
||||||
204
dependencies.py
204
dependencies.py
|
|
@ -1,204 +0,0 @@
|
||||||
import yaml
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def print_blue(text):
|
|
||||||
BLUE = '\x1b[34m'
|
|
||||||
RESET = '\x1b[0m'
|
|
||||||
print(f"{BLUE}{text}{RESET}")
|
|
||||||
|
|
||||||
def print_yellow(text):
|
|
||||||
YELLOW = '\x1b[33m'
|
|
||||||
RESET = '\x1b[0m'
|
|
||||||
print(f"{YELLOW}{text}{RESET}")
|
|
||||||
|
|
||||||
def get_git_head(repo_path='.'):
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'rev-parse', 'HEAD'],
|
|
||||||
cwd=repo_path,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
check=False
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise RuntimeError(f"git error: {result.stderr.strip()}")
|
|
||||||
return result.stdout.strip()
|
|
||||||
|
|
||||||
def get_default_branch(repo_path='.'):
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'symbolic-ref', 'refs/remotes/origin/HEAD'],
|
|
||||||
cwd=repo_path,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
check=False
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return result.stdout.strip().split('/')[-1]
|
|
||||||
return 'main'
|
|
||||||
|
|
||||||
def integrate_package(folder_name, data, cache_dir, out_dir):
|
|
||||||
repo_url = data['git']
|
|
||||||
keep_list = ["lib", "LICENSE", "pubspec.yaml", "android", "ios", "darwin"]
|
|
||||||
if "keep" in data:
|
|
||||||
keep_list += [item.rstrip('/') for item in data['keep']]
|
|
||||||
|
|
||||||
print(f"Processing {folder_name}...")
|
|
||||||
|
|
||||||
cache_path = os.path.join(cache_dir, folder_name)
|
|
||||||
if not os.path.exists(cache_path):
|
|
||||||
subprocess.run(["git", "clone", repo_url, cache_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
else:
|
|
||||||
result = subprocess.run(["git", "fetch", "--all"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
|
||||||
if result.returncode != 0:
|
|
||||||
print_yellow(f"Warning: Could not fetch updates for {folder_name}. You might be offline.")
|
|
||||||
|
|
||||||
if "commit" in data:
|
|
||||||
commit_hash = data["commit"]
|
|
||||||
subprocess.run(["git", "checkout", commit_hash], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
|
||||||
elif "tag" in data:
|
|
||||||
tag_name = data["tag"]
|
|
||||||
subprocess.run(["git", "checkout", tag_name], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
|
||||||
else:
|
|
||||||
print_yellow(f"Warning: No commit or tag specified for {folder_name}. Using default branch.")
|
|
||||||
default_branch = get_default_branch(cache_path)
|
|
||||||
subprocess.run(["git", "checkout", default_branch], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
|
||||||
subprocess.run(["git", "pull"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
|
||||||
last_commit_hash = get_git_head(cache_path)
|
|
||||||
data["commit"] = last_commit_hash
|
|
||||||
print_blue(f"Recorded commit {last_commit_hash} for {folder_name}")
|
|
||||||
|
|
||||||
results = [] # List of (pkg_name, version)
|
|
||||||
|
|
||||||
if "subpackages" in data:
|
|
||||||
packages_to_extract = data["subpackages"]
|
|
||||||
else:
|
|
||||||
packages_to_extract = [{"name": folder_name, "path": data.get("path", "")}]
|
|
||||||
|
|
||||||
for pkg in packages_to_extract:
|
|
||||||
pkg_name = pkg["name"]
|
|
||||||
subpath = pkg.get("path", "")
|
|
||||||
|
|
||||||
out_path = os.path.join(out_dir, pkg_name)
|
|
||||||
if os.path.exists(out_path):
|
|
||||||
shutil.rmtree(out_path)
|
|
||||||
os.makedirs(out_path)
|
|
||||||
|
|
||||||
package_src_path = os.path.join(cache_path, subpath) if subpath else cache_path
|
|
||||||
|
|
||||||
for item in keep_list:
|
|
||||||
src_item = os.path.join(package_src_path, item)
|
|
||||||
dst_item = os.path.join(out_path, item)
|
|
||||||
|
|
||||||
if os.path.exists(src_item):
|
|
||||||
os.makedirs(os.path.dirname(dst_item), exist_ok=True)
|
|
||||||
if os.path.isdir(src_item):
|
|
||||||
shutil.copytree(src_item, dst_item, dirs_exist_ok=True)
|
|
||||||
else:
|
|
||||||
shutil.copy2(src_item, dst_item)
|
|
||||||
|
|
||||||
version = "any"
|
|
||||||
try:
|
|
||||||
pubspec_path = os.path.join(package_src_path, "pubspec.yaml")
|
|
||||||
if os.path.exists(pubspec_path):
|
|
||||||
with open(pubspec_path, "r") as f:
|
|
||||||
ps = yaml.safe_load(f)
|
|
||||||
if ps and isinstance(ps, dict):
|
|
||||||
version = ps.get("version", "any")
|
|
||||||
except Exception as e:
|
|
||||||
print_yellow(f"Warning: Could not read version from {pkg_name}/pubspec.yaml")
|
|
||||||
|
|
||||||
results.append((pkg_name, version))
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Update specific or all repositories.")
|
|
||||||
parser.add_argument('repo_name', nargs='?', default=None, help="Name of the repository to update (optional)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
with open("dependencies.yaml", "r") as f:
|
|
||||||
config = yaml.safe_load(f)
|
|
||||||
|
|
||||||
cache_dir = config.get('cache', './.cache')
|
|
||||||
out_dir = config.get('outdir', './dependencies')
|
|
||||||
deps = config.get('dependencies', {})
|
|
||||||
|
|
||||||
if not os.path.exists(cache_dir):
|
|
||||||
os.makedirs(cache_dir)
|
|
||||||
if not os.path.exists(out_dir):
|
|
||||||
os.makedirs(out_dir)
|
|
||||||
|
|
||||||
repos_to_update = [args.repo_name] if args.repo_name else list(deps.keys())
|
|
||||||
|
|
||||||
pubspec_overrides = []
|
|
||||||
pubspec_deps = []
|
|
||||||
|
|
||||||
def process_deps_recursive(deps_dict, to_update=None):
|
|
||||||
for name, data in deps_dict.items():
|
|
||||||
if to_update is None or name in to_update:
|
|
||||||
extracted_packages = integrate_package(name, data, cache_dir, out_dir)
|
|
||||||
for pkg_name, version in extracted_packages:
|
|
||||||
pubspec_overrides.append(f" {pkg_name}:\n path: {out_dir}/{pkg_name}\n")
|
|
||||||
if version and version != "any":
|
|
||||||
pubspec_deps.append(f" {pkg_name}: ^{version}\n")
|
|
||||||
else:
|
|
||||||
pubspec_deps.append(f" {pkg_name}: any\n")
|
|
||||||
if "dependencies" in data:
|
|
||||||
# If we updated the parent, we should update children? Or if no args provided, update all.
|
|
||||||
# Actually, the original logic updated children automatically if parent is updated.
|
|
||||||
process_deps_recursive(data["dependencies"], None if (to_update is None or name in to_update) else [])
|
|
||||||
|
|
||||||
process_deps_recursive(deps, repos_to_update if args.repo_name else None)
|
|
||||||
|
|
||||||
def sort_dependencies(d):
|
|
||||||
sorted_d = {k: d[k] for k in sorted(d.keys())}
|
|
||||||
for k, v in sorted_d.items():
|
|
||||||
if "dependencies" in v and isinstance(v["dependencies"], dict):
|
|
||||||
v["dependencies"] = sort_dependencies(v["dependencies"])
|
|
||||||
return sorted_d
|
|
||||||
|
|
||||||
if "dependencies" in config:
|
|
||||||
config["dependencies"] = sort_dependencies(config["dependencies"])
|
|
||||||
|
|
||||||
with open("dependencies.yaml", "w") as f:
|
|
||||||
yaml.safe_dump(config, f, sort_keys=False)
|
|
||||||
|
|
||||||
# Update pubspec.yaml
|
|
||||||
if not args.repo_name:
|
|
||||||
with open("pubspec.yaml", "r") as f:
|
|
||||||
pubspec_lines = f.readlines()
|
|
||||||
|
|
||||||
start_marker_overrides = "## --- Start Managed Dependency Overrides ---"
|
|
||||||
end_marker_overrides = "## --- End Managed Dependency Overrides ---"
|
|
||||||
|
|
||||||
start_marker_deps = "## --- Start Managed Dependencies ---"
|
|
||||||
end_marker_deps = "## --- End Managed Dependencies ---"
|
|
||||||
|
|
||||||
try:
|
|
||||||
start_idx_overrides = next(i for i, line in enumerate(pubspec_lines) if line.strip() == start_marker_overrides)
|
|
||||||
end_idx_overrides = next(i for i, line in enumerate(pubspec_lines) if line.strip() == end_marker_overrides)
|
|
||||||
|
|
||||||
# Update overrides section
|
|
||||||
new_lines = pubspec_lines[:start_idx_overrides + 1] + pubspec_overrides + pubspec_lines[end_idx_overrides:]
|
|
||||||
|
|
||||||
# Now find the deps markers in the updated lines
|
|
||||||
start_idx_deps = next(i for i, line in enumerate(new_lines) if line.strip() == start_marker_deps)
|
|
||||||
end_idx_deps = next(i for i, line in enumerate(new_lines) if line.strip() == end_marker_deps)
|
|
||||||
|
|
||||||
# Update dependencies section
|
|
||||||
final_lines = new_lines[:start_idx_deps + 1] + pubspec_deps + new_lines[end_idx_deps:]
|
|
||||||
|
|
||||||
with open("pubspec.yaml", "w") as f:
|
|
||||||
f.writelines(final_lines)
|
|
||||||
print_blue("Updated pubspec.yaml successfully.")
|
|
||||||
except ValueError as e:
|
|
||||||
print_yellow("Error: Could not find professional markers in pubspec.yaml.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
cache: ./.cache
|
|
||||||
outdir: ./dependencies
|
|
||||||
dependencies:
|
|
||||||
audio_waveforms:
|
|
||||||
git: https://github.com/SimformSolutionsPvtLtd/audio_waveforms.git
|
|
||||||
tag: 2.0.2
|
|
||||||
avatar_maker:
|
|
||||||
git: https://github.com/RoadTripMoustache/avatar_maker.git
|
|
||||||
tag: 1.5.0
|
|
||||||
keep:
|
|
||||||
- assets/icons
|
|
||||||
blurhash_dart:
|
|
||||||
git: https://github.com/justacid/blurhash-dart.git
|
|
||||||
tag: v1.2.1
|
|
||||||
exif:
|
|
||||||
git: https://github.com/bigflood/dartexif.git
|
|
||||||
dependencies:
|
|
||||||
sprintf:
|
|
||||||
git: https://github.com/Naddiseo/dart-sprintf.git
|
|
||||||
commit: f1e74f2f4c339d983f9d011b4ba1df4ec8b8857c
|
|
||||||
commit: bf170d5639f0b6fcb0947060cf8bd7b623df9069
|
|
||||||
flutter_blurhash:
|
|
||||||
git: https://github.com/fluttercommunity/flutter_blurhash.git
|
|
||||||
commit: c5cdec4986432e835bb91f5ce00564534450cdc7
|
|
||||||
flutter_markdown_plus:
|
|
||||||
git: https://github.com/foresightmobile/flutter_markdown_plus.git
|
|
||||||
commit: dc1185c933fbf9dba559ef6c91586ff1503be3ee
|
|
||||||
flutter_packages:
|
|
||||||
git: https://github.com/otsmr/flutter-packages.git
|
|
||||||
subpackages:
|
|
||||||
- name: video_player
|
|
||||||
path: packages/video_player/video_player
|
|
||||||
- name: video_player_android
|
|
||||||
path: packages/video_player/video_player_android
|
|
||||||
- name: video_player_avfoundation
|
|
||||||
path: packages/video_player/video_player_avfoundation
|
|
||||||
- name: camera_android_camerax
|
|
||||||
path: packages/camera/camera_android_camerax
|
|
||||||
commit: b4f7d807a822a8ce327aae21353ac2217e928f6d
|
|
||||||
flutter_sharing_intent:
|
|
||||||
git: https://github.com/bhagat-techind/flutter_sharing_intent.git
|
|
||||||
commit: aa1672f547d6579585fa27df0b28ffa2a2544aaa
|
|
||||||
hand_signature:
|
|
||||||
git: https://github.com/RomanBase/hand_signature.git
|
|
||||||
commit: 1beedb164d093643365b0832277c377353c7464f
|
|
||||||
hashlib:
|
|
||||||
git: https://github.com/bitanon/hashlib.git
|
|
||||||
replace:
|
|
||||||
- - abstract class MACHashBase
|
|
||||||
- abstract mixin class MACHashBase
|
|
||||||
dependencies:
|
|
||||||
hashlib_codecs:
|
|
||||||
git: https://github.com/bitanon/hashlib_codecs.git
|
|
||||||
commit: 2a966c37c3b9b1f5541ae88e99ab34acf3fc968b
|
|
||||||
commit: bc9c2f8dd7bbc72f47ccab0ce1111d40259c49bc
|
|
||||||
image:
|
|
||||||
git: https://github.com/brendan-duncan/image.git
|
|
||||||
tag: v4.9.2
|
|
||||||
introduction_screen:
|
|
||||||
git: https://github.com/Pyozer/introduction_screen.git
|
|
||||||
dependencies:
|
|
||||||
dots_indicator:
|
|
||||||
git: https://github.com/Pyozer/dots_indicator.git
|
|
||||||
commit: 508f5883ac79bdbc10254092de3f28f571d261cd
|
|
||||||
commit: 4a90e557630b28834479ed9c64a9d2d0185d8e48
|
|
||||||
libsignal_protocol_dart:
|
|
||||||
git: https://github.com/MixinNetwork/libsignal_protocol_dart.git
|
|
||||||
dependencies:
|
|
||||||
adaptive_number:
|
|
||||||
git: https://github.com/lemoony/adaptive_number_dart
|
|
||||||
commit: ea9178fdd4d82ac45cf0ec966ac870dae661124f
|
|
||||||
ed25519_edwards:
|
|
||||||
git: https://github.com/Tougee/ed25519.git
|
|
||||||
commit: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
|
|
||||||
optional:
|
|
||||||
git: https://github.com/tonio-ramirez/optional.dart.git
|
|
||||||
commit: 71c638891ce4f2aff35c7387727989f31f9d877d
|
|
||||||
pointycastle:
|
|
||||||
git: https://github.com/bcgit/pc-dart.git
|
|
||||||
commit: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
|
||||||
x25519:
|
|
||||||
git: https://github.com/Tougee/curve25519.git
|
|
||||||
commit: ecb1d357714537bba6e276ef45f093846d4beaee
|
|
||||||
commit: c95a1586057022acdbb9c76b1692d94cc549bcc7
|
|
||||||
lottie:
|
|
||||||
git: https://github.com/xvrh/lottie-flutter.git
|
|
||||||
commit: 127bc29f2c6bd8b32ec4064a09e54e6b31cd0a88
|
|
||||||
mutex:
|
|
||||||
git: https://github.com/hoylen/dart-mutex.git
|
|
||||||
commit: 84ca903a3ac863735e3228c75a212133621f680f
|
|
||||||
photo_view:
|
|
||||||
git: https://github.com/bluefireteam/photo_view.git
|
|
||||||
commit: a13ca2fc387a3fb1276126959e092c44d0029987
|
|
||||||
pro_video_editor:
|
|
||||||
git: https://github.com/hm21/pro_video_editor.git
|
|
||||||
tag: v2.11.3
|
|
||||||
qr_flutter:
|
|
||||||
git: https://github.com/theyakka/qr.flutter.git
|
|
||||||
dependencies:
|
|
||||||
qr:
|
|
||||||
git: https://github.com/kevmoo/qr.dart.git
|
|
||||||
commit: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
|
|
||||||
commit: d5e7206396105d643113618290bbcc755d05f492
|
|
||||||
restart_app:
|
|
||||||
git: https://github.com/gabrimatic/restart_app
|
|
||||||
commit: 66897cb67e235bab85421647bfae036acb4438cb
|
|
||||||
screen_protector:
|
|
||||||
git: https://github.com/prongbang/screen_protector.git
|
|
||||||
commit: 019c04d622d7b610d2903d3a347edc3ba76a6ed0
|
|
||||||
|
|
@ -92,6 +92,8 @@ PODS:
|
||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- pro_video_editor (0.0.1):
|
||||||
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- rust_lib_twonly (0.0.1):
|
- rust_lib_twonly (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
@ -120,6 +122,7 @@ DEPENDENCIES:
|
||||||
- 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`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/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`)
|
||||||
- SwiftProtobuf
|
- SwiftProtobuf
|
||||||
|
|
@ -167,6 +170,8 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
pro_video_editor:
|
||||||
|
:path: ".symlinks/plugins/pro_video_editor/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:
|
||||||
|
|
@ -177,7 +182,7 @@ EXTERNAL SOURCES:
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
||||||
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
||||||
Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||||
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
|
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
|
||||||
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
||||||
|
|
@ -198,6 +203,7 @@ SPEC CHECKSUMS:
|
||||||
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
|
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
||||||
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
||||||
|
|
|
||||||
|
|
@ -742,7 +742,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
|
@ -886,7 +886,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|
@ -939,7 +939,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|
||||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||||
import 'package:twonly/src/providers/routing.provider.dart';
|
import 'package:twonly/src/providers/routing.provider.dart';
|
||||||
|
|
@ -181,17 +180,9 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
void handleShareMedia(String path, MediaType type) {
|
|
||||||
HomeViewState.pendingSharedMedia = (path, type);
|
|
||||||
routerProvider.go(Routes.home);
|
|
||||||
HomeViewState.streamHomeViewPageIndex.add(0);
|
|
||||||
HomeViewState.streamSharedMedia.add((path, type));
|
|
||||||
}
|
|
||||||
|
|
||||||
_intentStreamSub = initIntentStreams(
|
_intentStreamSub = initIntentStreams(
|
||||||
context,
|
context,
|
||||||
handleShareLink,
|
handleShareLink,
|
||||||
handleShareMedia,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)..where(groups.isDirectChat.equals(false)));
|
)..where(groups.isDirectChat.equals(false)));
|
||||||
return await query.map((row) => row.readTable(groupMembers)).get();
|
return query.map((row) => row.readTable(groupMembers)).get();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -346,9 +346,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
DateTime newLastMessage,
|
DateTime newLastMessage,
|
||||||
) async {
|
) async {
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final clampedLastMessage = newLastMessage.isAfter(now)
|
final clampedLastMessage =
|
||||||
? now
|
newLastMessage.isAfter(now) ? now : newLastMessage;
|
||||||
: newLastMessage;
|
|
||||||
await (update(groupMembers)..where(
|
await (update(groupMembers)..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.groupId.equals(groupId) &
|
t.groupId.equals(groupId) &
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
await (delete(receivedReceipts)..where(
|
await (delete(receivedReceipts)..where(
|
||||||
(t) => (t.createdAt.isSmallerThanValue(
|
(t) => (t.createdAt.isSmallerThanValue(
|
||||||
clock.now().subtract(
|
clock.now().subtract(
|
||||||
const Duration(days: 45),
|
const Duration(days: 25),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
|
|
@ -250,25 +250,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Claims a new delivery-receipt attempt after [cooldown] has elapsed.
|
|
||||||
///
|
|
||||||
/// Updating the timestamp before sending prevents repeated server batches from
|
|
||||||
/// starting multiple delivery-receipt attempts during the same cooldown.
|
|
||||||
Future<bool> claimDuplicateReceiptResend(
|
|
||||||
String receiptId,
|
|
||||||
Duration cooldown,
|
|
||||||
) async {
|
|
||||||
final now = clock.now();
|
|
||||||
final updated =
|
|
||||||
await (update(receivedReceipts)..where(
|
|
||||||
(t) =>
|
|
||||||
t.receiptId.equals(receiptId) &
|
|
||||||
t.createdAt.isSmallerOrEqualValue(now.subtract(cooldown)),
|
|
||||||
))
|
|
||||||
.write(ReceivedReceiptsCompanion(createdAt: Value(now)));
|
|
||||||
return updated > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> gotReceipt(String receiptId) async {
|
Future<void> gotReceipt(String receiptId) async {
|
||||||
await into(
|
await into(
|
||||||
receivedReceipts,
|
receivedReceipts,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
|
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
|
||||||
|
|
||||||
@DataClassName('Message')
|
@DataClassName('Message')
|
||||||
@TableIndex(name: 'idx_messages_group_id_created_at', columns: {#groupId, #createdAt})
|
|
||||||
class Messages extends Table {
|
class Messages extends Table {
|
||||||
TextColumn get groupId =>
|
TextColumn get groupId =>
|
||||||
text().references(Groups, #groupId, onDelete: KeyAction.cascade)();
|
text().references(Groups, #groupId, onDelete: KeyAction.cascade)();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
|
|
||||||
@DataClassName('Receipt')
|
@DataClassName('Receipt')
|
||||||
@TableIndex(name: 'idx_receipts_message_id', columns: {#messageId})
|
|
||||||
class Receipts extends Table {
|
class Receipts extends Table {
|
||||||
TextColumn get receiptId => text()();
|
TextColumn get receiptId => text()();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13843,14 +13843,6 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
||||||
);
|
);
|
||||||
late final $LabelsTable labels = $LabelsTable(this);
|
late final $LabelsTable labels = $LabelsTable(this);
|
||||||
late final $ContactLabelsTable contactLabels = $ContactLabelsTable(this);
|
late final $ContactLabelsTable contactLabels = $ContactLabelsTable(this);
|
||||||
late final Index idxMessagesGroupIdCreatedAt = Index(
|
|
||||||
'idx_messages_group_id_created_at',
|
|
||||||
'CREATE INDEX idx_messages_group_id_created_at ON messages (group_id, created_at)',
|
|
||||||
);
|
|
||||||
late final Index idxReceiptsMessageId = Index(
|
|
||||||
'idx_receipts_message_id',
|
|
||||||
'CREATE INDEX idx_receipts_message_id ON receipts (message_id)',
|
|
||||||
);
|
|
||||||
late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB);
|
late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB);
|
||||||
late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB);
|
late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB);
|
||||||
late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB);
|
late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB);
|
||||||
|
|
@ -13897,8 +13889,6 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
||||||
shortcutMembers,
|
shortcutMembers,
|
||||||
labels,
|
labels,
|
||||||
contactLabels,
|
contactLabels,
|
||||||
idxMessagesGroupIdCreatedAt,
|
|
||||||
idxReceiptsMessageId,
|
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
|
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
|
||||||
|
|
|
||||||
|
|
@ -1601,7 +1601,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @backupSelectStrongPassword.
|
/// No description provided for @backupSelectStrongPassword.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Select a secure password. It is recommended to generate and store it via your password manager of choice.'**
|
/// **'Choose a secure password. This is required if you want to restore your backup.'**
|
||||||
String get backupSelectStrongPassword;
|
String get backupSelectStrongPassword;
|
||||||
|
|
||||||
/// No description provided for @password.
|
/// No description provided for @password.
|
||||||
|
|
@ -1631,7 +1631,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @backupEnableBackup.
|
/// No description provided for @backupEnableBackup.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Enable Password Backup'**
|
/// **'Activate automatic backup'**
|
||||||
String get backupEnableBackup;
|
String get backupEnableBackup;
|
||||||
|
|
||||||
/// No description provided for @backupTwonlySaveNow.
|
/// No description provided for @backupTwonlySaveNow.
|
||||||
|
|
@ -3713,7 +3713,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @passwordlessRecovery.
|
/// No description provided for @passwordlessRecovery.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Trusted Friends'**
|
/// **'Password Recovery'**
|
||||||
String get passwordlessRecovery;
|
String get passwordlessRecovery;
|
||||||
|
|
||||||
/// No description provided for @passwordlessRecoveryNotConfigured.
|
/// No description provided for @passwordlessRecoveryNotConfigured.
|
||||||
|
|
@ -3833,7 +3833,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @passwordlessRecoveryEnableSuccess.
|
/// No description provided for @passwordlessRecoveryEnableSuccess.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Trusted Friends successfully set up!'**
|
/// **'Passwordless recovery successfully enabled!'**
|
||||||
String get passwordlessRecoveryEnableSuccess;
|
String get passwordlessRecoveryEnableSuccess;
|
||||||
|
|
||||||
/// No description provided for @passwordlessRecoveryEnterPin.
|
/// No description provided for @passwordlessRecoveryEnterPin.
|
||||||
|
|
@ -3851,19 +3851,19 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @passwordlessRecoveryEnableBtn.
|
/// No description provided for @passwordlessRecoveryEnableBtn.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Enable Trusted Friends'**
|
/// **'Enable Passwordless Recovery'**
|
||||||
String get passwordlessRecoveryEnableBtn;
|
String get passwordlessRecoveryEnableBtn;
|
||||||
|
|
||||||
/// No description provided for @passwordlessRecoveryRecoverBtn.
|
/// No description provided for @passwordlessRecoveryRecoverBtn.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Recover via Trusted Friends'**
|
/// **'Recover passwordless'**
|
||||||
String get passwordlessRecoveryRecoverBtn;
|
String get passwordlessRecoveryRecoverBtn;
|
||||||
|
|
||||||
/// No description provided for @passwordlessRecoveryModifyBtn.
|
/// No description provided for @passwordlessRecoveryModifyBtn.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Modify Trusted Friends'**
|
/// **'Modify Passwordless Recovery'**
|
||||||
String get passwordlessRecoveryModifyBtn;
|
String get passwordlessRecoveryModifyBtn;
|
||||||
|
|
||||||
/// No description provided for @passwordlessRecoveryStatusEnabled.
|
/// No description provided for @passwordlessRecoveryStatusEnabled.
|
||||||
|
|
@ -4435,108 +4435,6 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Dark'**
|
/// **'Dark'**
|
||||||
String get themeDark;
|
String get themeDark;
|
||||||
|
|
||||||
/// No description provided for @backupRecoverySectionTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Secure Account Access'**
|
|
||||||
String get backupRecoverySectionTitle;
|
|
||||||
|
|
||||||
/// No description provided for @backupRecoverySectionDescNone.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.'**
|
|
||||||
String get backupRecoverySectionDescNone;
|
|
||||||
|
|
||||||
/// No description provided for @backupRecoverySectionDescSome.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'You can recover your account with the recovery options below. It is recommended to enable both options.'**
|
|
||||||
String get backupRecoverySectionDescSome;
|
|
||||||
|
|
||||||
/// No description provided for @backupRecoveryOptionAFriends.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Via trusted friends'**
|
|
||||||
String get backupRecoveryOptionAFriends;
|
|
||||||
|
|
||||||
/// No description provided for @backupRecoveryOptionAMicrocopy.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Select friends who can help you recover your account.'**
|
|
||||||
String get backupRecoveryOptionAMicrocopy;
|
|
||||||
|
|
||||||
/// No description provided for @backupRecoveryOptionBPassword.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Classic Password'**
|
|
||||||
String get backupRecoveryOptionBPassword;
|
|
||||||
|
|
||||||
/// No description provided for @backupRecoveryOptionBMicrocopy.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Set a password for your access.'**
|
|
||||||
String get backupRecoveryOptionBMicrocopy;
|
|
||||||
|
|
||||||
/// No description provided for @backupCloudSectionTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Backup Your Data'**
|
|
||||||
String get backupCloudSectionTitle;
|
|
||||||
|
|
||||||
/// No description provided for @backupCloudSectionDesc.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Backup your content so nothing gets lost.'**
|
|
||||||
String get backupCloudSectionDesc;
|
|
||||||
|
|
||||||
/// No description provided for @backupCloudContactsMessages.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Contacts & Messages'**
|
|
||||||
String get backupCloudContactsMessages;
|
|
||||||
|
|
||||||
/// No description provided for @backupCloudFreeActive.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Free Active'**
|
|
||||||
String get backupCloudFreeActive;
|
|
||||||
|
|
||||||
/// No description provided for @backupCloudImagesMedia.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Images & Media'**
|
|
||||||
String get backupCloudImagesMedia;
|
|
||||||
|
|
||||||
/// No description provided for @backupCloudProBadge.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Pro'**
|
|
||||||
String get backupCloudProBadge;
|
|
||||||
|
|
||||||
/// No description provided for @memoriesAddingToFavorites.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Adding to favorites...'**
|
|
||||||
String get memoriesAddingToFavorites;
|
|
||||||
|
|
||||||
/// No description provided for @memoriesRemovingFromFavorites.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Removing from favorites...'**
|
|
||||||
String get memoriesRemovingFromFavorites;
|
|
||||||
|
|
||||||
/// No description provided for @memoriesDeletingProgress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Deleting memories...'**
|
|
||||||
String get memoriesDeletingProgress;
|
|
||||||
|
|
||||||
/// No description provided for @memoriesExportingProgress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Exporting memories...'**
|
|
||||||
String get memoriesExportingProgress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -850,7 +850,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupSelectStrongPassword =>
|
String get backupSelectStrongPassword =>
|
||||||
'Wähle ein sicheres Passwort. Es wird empfohlen, dieses über deinen bevorzugten Passwort-Manager zu generieren und zu speichern.';
|
'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein Backup wiederherstellen möchtest.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get password => 'Passwort';
|
String get password => 'Passwort';
|
||||||
|
|
@ -866,7 +866,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupEnableBackup => 'Passwort-Backup aktivieren';
|
String get backupEnableBackup => 'Automatische Sicherung aktivieren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupTwonlySaveNow => 'Jetzt speichern';
|
String get backupTwonlySaveNow => 'Jetzt speichern';
|
||||||
|
|
@ -2144,7 +2144,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get avatarCustomizeReset => 'Zurücksetzen';
|
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecovery => 'Vertraute Freunde';
|
String get passwordlessRecovery => 'Passwort vergessen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
||||||
|
|
@ -2210,7 +2210,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnableSuccess =>
|
String get passwordlessRecoveryEnableSuccess =>
|
||||||
'Vertraute Freunde erfolgreich eingerichtet!';
|
'\"Passwort vergessen\" erfolgreich aktiviert!';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
||||||
|
|
@ -2220,14 +2220,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Bitte gib eine E-Mail-Adresse ein.';
|
'Bitte gib eine E-Mail-Adresse ein.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnableBtn => 'Vertraute Freunde aktivieren';
|
String get passwordlessRecoveryEnableBtn =>
|
||||||
|
'\"Passwort vergessen\" aktivieren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryRecoverBtn =>
|
String get passwordlessRecoveryRecoverBtn =>
|
||||||
'Mithilfe von vertrauten Freunden wiederherstellen';
|
'Mit \"Passwort vergessen\" wiederherstellen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryModifyBtn => 'Vertraute Freunde bearbeiten';
|
String get passwordlessRecoveryModifyBtn =>
|
||||||
|
'\"Passwort vergessen\" bearbeiten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String passwordlessRecoveryStatusEnabled(num count) {
|
String passwordlessRecoveryStatusEnabled(num count) {
|
||||||
|
|
@ -2566,60 +2568,4 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get themeDark => 'Dunkel';
|
String get themeDark => 'Dunkel';
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoverySectionTitle => 'Konto-Zugang sichern';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoverySectionDescNone =>
|
|
||||||
'Du musst mindestens eine Wiederherstellungsoption auswählen, da dir sonst niemand helfen kann, falls du den Zugriff auf dein Gerät verlierst.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoverySectionDescSome =>
|
|
||||||
'Mit den untenstehenden Wiederherstellungsoptionen kannst du dein Konto wiederherstellen. Es wird empfohlen, beide Optionen zu aktivieren.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionAFriends => 'Über vertraute Freunde';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionAMicrocopy =>
|
|
||||||
'Wähle Freunde aus, die dir helfen, dein Konto wiederherzustellen.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionBPassword => 'Klassisches Passwort';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionBMicrocopy =>
|
|
||||||
'Lege ein Passwort für deinen Zugang fest.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudSectionTitle => 'Deine Daten sichern';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudSectionDesc =>
|
|
||||||
'Sichere deine Inhalte, damit nichts verloren geht.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudContactsMessages => 'Kontakte & Nachrichten';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudFreeActive => 'Kostenlos aktiv';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudImagesMedia => 'Bilder & Medien';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudProBadge => 'Pro';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesAddingToFavorites => 'Zu Favoriten hinzufügen...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesRemovingFromFavorites => 'Aus Favoriten entfernen...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesDeletingProgress => 'Erinnerungen werden gelöscht...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesExportingProgress => 'Erinnerungen werden exportiert...';
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -845,7 +845,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupSelectStrongPassword =>
|
String get backupSelectStrongPassword =>
|
||||||
'Select a secure password. It is recommended to generate and store it via your password manager of choice.';
|
'Choose a secure password. This is required if you want to restore your backup.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get password => 'Password';
|
String get password => 'Password';
|
||||||
|
|
@ -861,7 +861,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
'Password must be at least 10 characters long.';
|
'Password must be at least 10 characters long.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupEnableBackup => 'Enable Password Backup';
|
String get backupEnableBackup => 'Activate automatic backup';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupTwonlySaveNow => 'Save now';
|
String get backupTwonlySaveNow => 'Save now';
|
||||||
|
|
@ -2129,7 +2129,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get avatarCustomizeReset => 'Reset';
|
String get avatarCustomizeReset => 'Reset';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecovery => 'Trusted Friends';
|
String get passwordlessRecovery => 'Password Recovery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
||||||
|
|
@ -2195,7 +2195,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnableSuccess =>
|
String get passwordlessRecoveryEnableSuccess =>
|
||||||
'Trusted Friends successfully set up!';
|
'Passwordless recovery successfully enabled!';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnterPin => 'Please enter a PIN.';
|
String get passwordlessRecoveryEnterPin => 'Please enter a PIN.';
|
||||||
|
|
@ -2204,13 +2204,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get passwordlessRecoveryEnterEmail => 'Please enter an email address.';
|
String get passwordlessRecoveryEnterEmail => 'Please enter an email address.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnableBtn => 'Enable Trusted Friends';
|
String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryRecoverBtn => 'Recover via Trusted Friends';
|
String get passwordlessRecoveryRecoverBtn => 'Recover passwordless';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryModifyBtn => 'Modify Trusted Friends';
|
String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String passwordlessRecoveryStatusEnabled(num count) {
|
String passwordlessRecoveryStatusEnabled(num count) {
|
||||||
|
|
@ -2541,60 +2541,4 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get themeDark => 'Dark';
|
String get themeDark => 'Dark';
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoverySectionTitle => 'Secure Account Access';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoverySectionDescNone =>
|
|
||||||
'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoverySectionDescSome =>
|
|
||||||
'You can recover your account with the recovery options below. It is recommended to enable both options.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionAFriends => 'Via trusted friends';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionAMicrocopy =>
|
|
||||||
'Select friends who can help you recover your account.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionBPassword => 'Classic Password';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupRecoveryOptionBMicrocopy =>
|
|
||||||
'Set a password for your access.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudSectionTitle => 'Backup Your Data';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudSectionDesc =>
|
|
||||||
'Backup your content so nothing gets lost.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudContactsMessages => 'Contacts & Messages';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudFreeActive => 'Free Active';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudImagesMedia => 'Images & Media';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backupCloudProBadge => 'Pro';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesAddingToFavorites => 'Adding to favorites...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesRemovingFromFavorites => 'Removing from favorites...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesDeletingProgress => 'Deleting memories...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get memoriesExportingProgress => 'Exporting memories...';
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486
|
Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5
|
||||||
|
|
@ -23,6 +23,7 @@ class UserData {
|
||||||
String username;
|
String username;
|
||||||
String displayName;
|
String displayName;
|
||||||
String? avatarSvg;
|
String? avatarSvg;
|
||||||
|
String? avatarJson;
|
||||||
|
|
||||||
@JsonKey(defaultValue: 0)
|
@JsonKey(defaultValue: 0)
|
||||||
int appVersion = 0;
|
int appVersion = 0;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
|
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
|
||||||
)
|
)
|
||||||
..avatarSvg = json['avatarSvg'] as String?
|
..avatarSvg = json['avatarSvg'] as String?
|
||||||
|
..avatarJson = json['avatarJson'] as String?
|
||||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||||
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
||||||
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
||||||
|
|
@ -128,6 +129,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'username': instance.username,
|
'username': instance.username,
|
||||||
'displayName': instance.displayName,
|
'displayName': instance.displayName,
|
||||||
'avatarSvg': instance.avatarSvg,
|
'avatarSvg': instance.avatarSvg,
|
||||||
|
'avatarJson': instance.avatarJson,
|
||||||
'appVersion': instance.appVersion,
|
'appVersion': instance.appVersion,
|
||||||
'avatarCounter': instance.avatarCounter,
|
'avatarCounter': instance.avatarCounter,
|
||||||
'isDeveloper': instance.isDeveloper,
|
'isDeveloper': instance.isDeveloper,
|
||||||
|
|
|
||||||
|
|
@ -299,17 +299,9 @@ class ApiService {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> sendResponse(ClientToServer response) async {
|
Future<void> sendResponse(ClientToServer response) async {
|
||||||
final channel = _channel;
|
if (_channel != null) {
|
||||||
if (channel == null || channel.closeCode != null) {
|
_channel!.sink.add(response.writeToBuffer());
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
channel.sink.add(response.writeToBuffer());
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
Log.warn('Could not send response to server: $e');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,19 +227,11 @@ Future<void> finishStartedPreprocessing() async {
|
||||||
mediaFile.mediaId,
|
mediaFile.mediaId,
|
||||||
);
|
);
|
||||||
if (messages.isEmpty) {
|
if (messages.isEmpty) {
|
||||||
if (mediaFile.createdAt.isBefore(
|
|
||||||
clock.now().subtract(const Duration(hours: 1)),
|
|
||||||
)) {
|
|
||||||
Log.info(
|
Log.info(
|
||||||
'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.',
|
'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.',
|
||||||
);
|
);
|
||||||
MediaFileService(mediaFile).fullMediaRemoval();
|
MediaFileService(mediaFile).fullMediaRemoval();
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
||||||
} else {
|
|
||||||
Log.info(
|
|
||||||
'Media file ${mediaFile.mediaId} has no messages, but is too new to be deleted by finishStartedPreprocessing. Skipping.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -492,20 +484,12 @@ Future<void> _startBackgroundMediaUploadInternal(
|
||||||
mediaService.mediaFile.mediaId,
|
mediaService.mediaFile.mediaId,
|
||||||
);
|
);
|
||||||
if (messages.isEmpty) {
|
if (messages.isEmpty) {
|
||||||
if (mediaService.mediaFile.createdAt.isBefore(
|
|
||||||
clock.now().subtract(const Duration(hours: 1)),
|
|
||||||
)) {
|
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path. Removing it from DB as files are not existent.',
|
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path. Removing it from DB as files are not existent.',
|
||||||
);
|
);
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(
|
await twonlyDB.mediaFilesDao.deleteMediaFile(
|
||||||
mediaService.mediaFile.mediaId,
|
mediaService.mediaFile.mediaId,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
Log.warn(
|
|
||||||
'Media files ${mediaService.mediaFile.mediaId} has no paths, but is too new to be deleted. Skipping deletion.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path, but messages still reference it. Marking as uploaded to stop retries.',
|
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path, but messages still reference it. Marking as uploaded to stop retries.',
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,22 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
receipt = loadedReceipt;
|
receipt = loadedReceipt;
|
||||||
|
|
||||||
|
if (receipt.retryCount >= 2) {
|
||||||
|
// After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received,
|
||||||
|
// but the app was closed before the message was fully processed. Because the receipt was already stored, subsequent retries were
|
||||||
|
// detected as duplicates and rejected.
|
||||||
|
final oldReceiptId = receipt.receiptId;
|
||||||
|
final updatedReceipt = await twonlyDB.receiptsDao.rotateReceiptId(
|
||||||
|
oldReceiptId,
|
||||||
|
);
|
||||||
|
if (updatedReceipt != null) {
|
||||||
|
Log.info(
|
||||||
|
'Changed receiptId $oldReceiptId to ${updatedReceipt.receiptId} as retryCount is ${receipt.retryCount}',
|
||||||
|
);
|
||||||
|
receipt = updatedReceipt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final contact = await twonlyDB.contactsDao.getContactById(
|
final contact = await twonlyDB.contactsDao.getContactById(
|
||||||
receipt.contactId,
|
receipt.contactId,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,9 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
||||||
..seq = msg.v0.seq
|
..seq = msg.v0.seq
|
||||||
..response = response;
|
..response = response;
|
||||||
|
|
||||||
final responseSent = await apiService.sendResponse(ClientToServer()..v0 = v0);
|
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||||
if (responseSent) {
|
|
||||||
Log.info(
|
|
||||||
'Successfully queued response for server message ${msg.v0.seq}.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Log.warn('Could not send response for server message ${msg.v0.seq}.');
|
|
||||||
}
|
|
||||||
AppState.gotMessageFromServer = true;
|
AppState.gotMessageFromServer = true;
|
||||||
Log.info('All messages from the server processed.');
|
Log.info('All messages from the server proccessed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
||||||
|
|
@ -148,43 +141,6 @@ Future<void> _handleClient2ClientMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
|
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
|
||||||
if (message.type == Message_Type.SENDER_DELIVERY_RECEIPT) {
|
|
||||||
Log.info(
|
|
||||||
'[$receiptId] Delivery receipt is a duplicate. Skipping receipt response.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicateReceiptCooldown = Duration(days: 10);
|
|
||||||
final shouldResend = await twonlyDB.receiptsDao.claimDuplicateReceiptResend(
|
|
||||||
receiptId,
|
|
||||||
duplicateReceiptCooldown,
|
|
||||||
);
|
|
||||||
if (!shouldResend) {
|
|
||||||
Log.info(
|
|
||||||
'[$receiptId] Message is a duplicate. Skipping delivery receipt during cooldown.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Log.info(
|
|
||||||
'[$receiptId] Message is a duplicate and cooldown elapsed. Sending delivery receipt again.',
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
final response = Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
|
|
||||||
await twonlyDB.receiptsDao.insertReceipt(
|
|
||||||
ReceiptsCompanion(
|
|
||||||
receiptId: Value(receiptId),
|
|
||||||
contactId: Value(fromUserId),
|
|
||||||
message: Value(response.writeToBuffer()),
|
|
||||||
contactWillSendsReceipt: const Value(false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Log.warn('[$receiptId] Error handling duplicate receipt ACK: $e');
|
|
||||||
}
|
|
||||||
await tryToSendCompleteMessage(
|
|
||||||
receiptId: receiptId,
|
|
||||||
blocking: false,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,14 +269,13 @@ Future<void> _handleClient2ClientMessage(
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.warn('[$receiptId] Error inserting receipt: $e');
|
Log.warn('[$receiptId] Error inserting receipt: $e');
|
||||||
}
|
}
|
||||||
|
if (targetReceiptId != null) {
|
||||||
targetReceiptId ??= receiptIdDB?.value ?? receiptId;
|
|
||||||
|
|
||||||
await tryToSendCompleteMessage(
|
await tryToSendCompleteMessage(
|
||||||
receiptId: targetReceiptId,
|
receiptId: targetReceiptId,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case Message_Type.TEST_NOTIFICATION:
|
case Message_Type.TEST_NOTIFICATION:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
||||||
}
|
}
|
||||||
// the state is now updated and the appended_group_state should be removed on the server, so just call this
|
// the state is now updated and the appended_group_state should be removed on the server, so just call this
|
||||||
// function again, to sync the local database
|
// function again, to sync the local database
|
||||||
return await fetchGroupState(group);
|
return fetchGroupState(group);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -10,6 +12,7 @@ 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/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||||
import 'package:twonly/src/services/passwordless_recovery.service.dart'
|
import 'package:twonly/src/services/passwordless_recovery.service.dart'
|
||||||
show PasswordlessRecoveryService;
|
show PasswordlessRecoveryService;
|
||||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||||
|
|
@ -17,6 +20,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/utils/qr.utils.dart';
|
import 'package:twonly/src/utils/qr.utils.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_contact_via_qr_link.view.dart';
|
import 'package:twonly/src/visual/views/contact/add_contact_via_qr_link.view.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
|
import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
|
||||||
|
|
||||||
|
|
@ -145,20 +149,53 @@ Future<void> _pubKeysDoNotMatch(BuildContext context, String username) async {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleIntentMediaFile(
|
||||||
|
BuildContext context,
|
||||||
|
String filePath,
|
||||||
|
MediaType type,
|
||||||
|
) async {
|
||||||
|
final file = File(filePath);
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
Log.error('The shared intent file does not exits.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newMediaService = await initializeMediaUpload(
|
||||||
|
type,
|
||||||
|
userService.currentUser.defaultShowTime,
|
||||||
|
);
|
||||||
|
if (newMediaService == null) {
|
||||||
|
Log.error('Could not create new media file for intent shared file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.copySync(newMediaService.originalPath.path);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ShareImageEditorView(
|
||||||
|
mediaFileService: newMediaService,
|
||||||
|
sharedFromGallery: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
StreamSubscription<List<SharedFile>> initIntentStreams(
|
StreamSubscription<List<SharedFile>> initIntentStreams(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
void Function(Uri) onUrlCallBack,
|
void Function(Uri) onUrlCallBack,
|
||||||
void Function(String, MediaType) onMediaCallBack,
|
|
||||||
) {
|
) {
|
||||||
FlutterSharingIntent.instance.getInitialSharing().then((f) {
|
FlutterSharingIntent.instance.getInitialSharing().then((f) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
handleIntentSharedFile(context, f, onUrlCallBack);
|
||||||
});
|
});
|
||||||
|
|
||||||
return FlutterSharingIntent.instance.getMediaStream().listen(
|
return FlutterSharingIntent.instance.getMediaStream().listen(
|
||||||
(f) {
|
(f) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
handleIntentSharedFile(context, f, onUrlCallBack);
|
||||||
},
|
},
|
||||||
// ignore: inference_failure_on_untyped_parameter
|
// ignore: inference_failure_on_untyped_parameter
|
||||||
onError: (err) {
|
onError: (err) {
|
||||||
|
|
@ -171,7 +208,6 @@ Future<void> handleIntentSharedFile(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<SharedFile> files,
|
List<SharedFile> files,
|
||||||
void Function(Uri) onUrlCallBack,
|
void Function(Uri) onUrlCallBack,
|
||||||
void Function(String, MediaType) onMediaCallBack,
|
|
||||||
) async {
|
) async {
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
if (file.value == null) {
|
if (file.value == null) {
|
||||||
|
|
@ -195,9 +231,9 @@ Future<void> handleIntentSharedFile(
|
||||||
if (file.value!.endsWith('.gif')) {
|
if (file.value!.endsWith('.gif')) {
|
||||||
type = MediaType.gif;
|
type = MediaType.gif;
|
||||||
}
|
}
|
||||||
onMediaCallBack(file.value!, type);
|
await handleIntentMediaFile(context, file.value!, type);
|
||||||
case SharedMediaType.VIDEO:
|
case SharedMediaType.VIDEO:
|
||||||
onMediaCallBack(file.value!, MediaType.video);
|
await handleIntentMediaFile(context, file.value!, MediaType.video);
|
||||||
// ignore: no_default_cases
|
// ignore: no_default_cases
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,11 +202,11 @@ class PasswordlessRecoveryService {
|
||||||
case SecondFactorType.pin:
|
case SecondFactorType.pin:
|
||||||
|
|
||||||
// The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin
|
// The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin
|
||||||
config
|
config.serverKeyProtection = getRandomUint8List(32);
|
||||||
..serverKeyProtection = getRandomUint8List(32)
|
|
||||||
// As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the
|
// As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the
|
||||||
// unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion.
|
// unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion.
|
||||||
..pinUnlockToken = getRandomUint8List(32);
|
config.pinUnlockToken = getRandomUint8List(32);
|
||||||
|
|
||||||
// Brute-force protection for the user's pin:
|
// Brute-force protection for the user's pin:
|
||||||
// - Server: Does not know the seed.
|
// - Server: Does not know the seed.
|
||||||
|
|
|
||||||
|
|
@ -140,9 +140,6 @@ signalDecryptMessageV1(
|
||||||
) = await lockingSignalProtocol.protect(() async {
|
) = await lockingSignalProtocol.protect(() async {
|
||||||
Log.info('Lock acquired for $fromUserId (V1)');
|
Log.info('Lock acquired for $fromUserId (V1)');
|
||||||
try {
|
try {
|
||||||
// Yield execution to the event loop to prevent UI freezing during bulk decryption
|
|
||||||
await Future.delayed(Duration.zero);
|
|
||||||
|
|
||||||
final session = SessionCipher.fromStore(
|
final session = SessionCipher.fromStore(
|
||||||
(await getSignalStore())!,
|
(await getSignalStore())!,
|
||||||
getSignalAddress(fromUserId),
|
getSignalAddress(fromUserId),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:mutex/mutex.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/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -115,14 +114,11 @@ File currentUserAvatarFile(int avatarCounter) {
|
||||||
return File('${avatarsDirectory.path}/user_$avatarCounter.png');
|
return File('${avatarsDirectory.path}/user_$avatarCounter.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
final _avatarMutex = Mutex();
|
|
||||||
|
|
||||||
Future<String?> getUserAvatar() async {
|
Future<String?> getUserAvatar() async {
|
||||||
if (userService.currentUser.avatarSvg == null) {
|
if (userService.currentUser.avatarSvg == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _avatarMutex.protect(() async {
|
|
||||||
final avatarCounter = userService.currentUser.avatarCounter;
|
final avatarCounter = userService.currentUser.avatarCounter;
|
||||||
final file = currentUserAvatarFile(avatarCounter);
|
final file = currentUserAvatarFile(avatarCounter);
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
|
|
@ -139,9 +135,8 @@ Future<String?> getUserAvatar() async {
|
||||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
final pngBytes = byteData!.buffer.asUint8List();
|
final pngBytes = byteData!.buffer.asUint8List();
|
||||||
|
|
||||||
await file.writeAsBytes(pngBytes, flush: true);
|
await file.writeAsBytes(pngBytes);
|
||||||
pictureInfo.picture.dispose();
|
pictureInfo.picture.dispose();
|
||||||
|
|
||||||
return file.path;
|
return file.path;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
|
|
@ -7,6 +6,7 @@ import 'package:convert/convert.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
@ -44,7 +44,6 @@ Future<String?> saveImageToGallery(
|
||||||
|
|
||||||
if (createdAt != null) {
|
if (createdAt != null) {
|
||||||
try {
|
try {
|
||||||
bytesToProcess = await Isolate.run(() {
|
|
||||||
final image = img.decodeImage(imageBytes);
|
final image = img.decodeImage(imageBytes);
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
final formattedDate = DateFormat(
|
final formattedDate = DateFormat(
|
||||||
|
|
@ -60,25 +59,26 @@ Future<String?> saveImageToGallery(
|
||||||
formattedDate,
|
formattedDate,
|
||||||
); // DateTimeDigitized
|
); // DateTimeDigitized
|
||||||
|
|
||||||
return img.encodeJpg(image);
|
bytesToProcess = img.encodeJpg(image);
|
||||||
}
|
}
|
||||||
return imageBytes;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final jpgImages = await FlutterImageCompress.compressWithList(
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
|
format: CompressFormat.jpeg,
|
||||||
|
bytesToProcess,
|
||||||
|
quality: 100,
|
||||||
|
keepExif: true,
|
||||||
|
);
|
||||||
final hasAccess = await Gal.hasAccess(toAlbum: true);
|
final hasAccess = await Gal.hasAccess(toAlbum: true);
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
await Gal.requestAccess(toAlbum: true);
|
await Gal.requestAccess(toAlbum: true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await Gal.putImageBytes(
|
await Gal.putImageBytes(jpgImages, album: 'twonly', name: name ?? 'image');
|
||||||
bytesToProcess,
|
|
||||||
album: 'twonly',
|
|
||||||
name: name ?? 'image',
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
} on GalException catch (e) {
|
} on GalException catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
|
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
class CachedNetworkImage extends StatefulWidget {
|
|
||||||
const CachedNetworkImage({
|
|
||||||
required this.imageUrl,
|
|
||||||
super.key,
|
|
||||||
this.width,
|
|
||||||
this.height,
|
|
||||||
this.fit,
|
|
||||||
this.placeholder,
|
|
||||||
this.errorWidget,
|
|
||||||
});
|
|
||||||
final String imageUrl;
|
|
||||||
final double? width;
|
|
||||||
final double? height;
|
|
||||||
final BoxFit? fit;
|
|
||||||
final Widget Function(BuildContext, String)? placeholder;
|
|
||||||
final Widget Function(BuildContext, String, dynamic)? errorWidget;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CachedNetworkImage> createState() => _CachedNetworkImageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CachedNetworkImageState extends State<CachedNetworkImage> {
|
|
||||||
File? _imageFile;
|
|
||||||
bool _isLoading = true;
|
|
||||||
dynamic _error;
|
|
||||||
static bool _hasCleanedUp = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(CachedNetworkImage oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.imageUrl != widget.imageUrl) {
|
|
||||||
_loadImage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadImage() async {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final cacheDir = await getTemporaryDirectory();
|
|
||||||
final urlHash = md5.convert(utf8.encode(widget.imageUrl)).toString();
|
|
||||||
final file = File('${cacheDir.path}/custom_cached_image_$urlHash');
|
|
||||||
|
|
||||||
if (!_hasCleanedUp) {
|
|
||||||
_hasCleanedUp = true;
|
|
||||||
unawaited(_cleanupOldFiles(cacheDir)); // Run asynchronously
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.existsSync()) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_imageFile = file;
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final client = HttpClient();
|
|
||||||
final request = await client.getUrl(Uri.parse(widget.imageUrl));
|
|
||||||
final response = await request.close();
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
await response.pipe(file.openWrite());
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_imageFile = file;
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to load image: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_error = e;
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _cleanupOldFiles(Directory cacheDir) async {
|
|
||||||
try {
|
|
||||||
final files = cacheDir.listSync();
|
|
||||||
final now = DateTime.now();
|
|
||||||
for (final f in files) {
|
|
||||||
if (f is File && f.path.contains('custom_cached_image_')) {
|
|
||||||
final stat = f.statSync();
|
|
||||||
if (now.difference(stat.modified).inDays >= 7) {
|
|
||||||
await f.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// Ignore cleanup errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (_isLoading) {
|
|
||||||
if (widget.placeholder != null) {
|
|
||||||
return widget.placeholder!(context, widget.imageUrl);
|
|
||||||
}
|
|
||||||
return SizedBox(width: widget.width, height: widget.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_error != null || _imageFile == null) {
|
|
||||||
if (widget.errorWidget != null) {
|
|
||||||
return widget.errorWidget!(context, widget.imageUrl, _error);
|
|
||||||
}
|
|
||||||
return SizedBox(width: widget.width, height: widget.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Image.file(
|
|
||||||
_imageFile!,
|
|
||||||
width: widget.width,
|
|
||||||
height: widget.height,
|
|
||||||
fit: widget.fit,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
if (widget.errorWidget != null) {
|
|
||||||
return widget.errorWidget!(context, widget.imageUrl, error);
|
|
||||||
}
|
|
||||||
return SizedBox(width: widget.width, height: widget.height);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -24,7 +24,9 @@ class _VideoPlayerFileHelperState extends State<VideoPlayerFileHelper> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = VideoPlayerController.file(
|
_controller = VideoPlayerController.file(
|
||||||
widget.videoPath,
|
widget.videoPath,
|
||||||
videoPlayerOptions: VideoPlayerOptions(),
|
videoPlayerOptions: VideoPlayerOptions(
|
||||||
|
mixWithOthers: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
|
|
|
||||||
|
|
@ -780,18 +780,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
mc.sharedLinkForPreview != null &&
|
mc.sharedLinkForPreview != null &&
|
||||||
mc.sharedLinkForPreview!.shouldGeneratePreview &&
|
mc.sharedLinkForPreview!.shouldGeneratePreview &&
|
||||||
!mc.isVideoRecording)
|
!mc.isVideoRecording)
|
||||||
Positioned(
|
ShowTitleText(
|
||||||
top: 50,
|
title: mc.sharedLinkForPreview!.url.host,
|
||||||
left: 0,
|
desc: 'Link',
|
||||||
right: 0,
|
isLink: true,
|
||||||
child: Center(
|
|
||||||
child: Chip(
|
|
||||||
label: Text(mc.sharedLinkForPreview!.url.host),
|
|
||||||
onDeleted: () {
|
|
||||||
mc.setSharedLinkForPreview(null);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown &&
|
if (!mc.isSharePreviewIsShown &&
|
||||||
!mc.isVideoRecording &&
|
!mc.isVideoRecording &&
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,10 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (_imageSaved)
|
|
||||||
const Icon(Icons.check, size: 14)
|
|
||||||
else
|
else
|
||||||
const FaIcon(FontAwesomeIcons.floppyDisk, size: 14),
|
_imageSaved
|
||||||
|
? const Icon(Icons.check, size: 14)
|
||||||
|
: 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(
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
});
|
});
|
||||||
videoController = VideoPlayerController.file(
|
videoController = VideoPlayerController.file(
|
||||||
mediaService.originalPath,
|
mediaService.originalPath,
|
||||||
videoPlayerOptions: VideoPlayerOptions(),
|
videoPlayerOptions: VideoPlayerOptions(
|
||||||
|
mixWithOthers: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
videoController?.setLooping(true);
|
videoController?.setLooping(true);
|
||||||
videoController
|
videoController
|
||||||
|
|
@ -125,7 +127,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
await videoController!.play();
|
await videoController!.play();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
})
|
})
|
||||||
// ignore: argument_type_not_assignable_to_error_handler
|
// ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error
|
||||||
.catchError(Log.error);
|
.catchError(Log.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart';
|
||||||
|
|
||||||
class ImageFilter extends StatelessWidget {
|
class ImageFilter extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
class CustomLinkCard extends StatelessWidget {
|
class CustomLinkCard extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
|
||||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
|
||||||
// Assuming the same Metadata import structure
|
// Assuming the same Metadata import structure
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
class YouTubePostCard extends StatelessWidget {
|
class YouTubePostCard extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
contactSub?.cancel();
|
contactSub?.cancel();
|
||||||
groupActionsSub?.cancel();
|
groupActionsSub?.cancel();
|
||||||
_nextTypingIndicator?.cancel();
|
_nextTypingIndicator?.cancel();
|
||||||
try {
|
|
||||||
textFieldFocus?.dispose();
|
|
||||||
// ignore: empty_catches
|
|
||||||
} catch (e) {}
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.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/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_reaction_row.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart';
|
||||||
|
|
@ -203,22 +202,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: right
|
|
||||||
? CrossAxisAlignment.end
|
|
||||||
: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
if (info.displayUserName != '' && !widget.group.isDirectChat)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2, left: 4),
|
|
||||||
child: Text(
|
|
||||||
info.displayUserName,
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.color.onSurfaceVariant,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ResponseContainer(
|
ResponseContainer(
|
||||||
msg: widget.message,
|
msg: widget.message,
|
||||||
group: widget.group,
|
group: widget.group,
|
||||||
|
|
@ -272,10 +256,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
: MainAxisAlignment.start,
|
: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!right && !widget.group.isDirectChat)
|
if (!right && !widget.group.isDirectChat)
|
||||||
if (hideContactAvatar)
|
hideContactAvatar
|
||||||
const SizedBox(width: 24)
|
? const SizedBox(width: 24)
|
||||||
else
|
: GestureDetector(
|
||||||
GestureDetector(
|
|
||||||
onTap: () => context.push(
|
onTap: () => context.push(
|
||||||
Routes.profileContact(widget.message.senderId!),
|
Routes.profileContact(widget.message.senderId!),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,18 @@ class ChatTextEntry extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (info.displayUserName != '')
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
info.displayUserName,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,8 @@ import 'package:clock/clock.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:intl/intl.dart' show DateFormat;
|
import 'package:intl/intl.dart' show DateFormat;
|
||||||
import 'package:twonly/locator.dart';
|
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
|
||||||
|
|
||||||
class FriendlyMessageTime extends StatelessWidget {
|
class FriendlyMessageTime extends StatelessWidget {
|
||||||
const FriendlyMessageTime({
|
const FriendlyMessageTime({
|
||||||
|
|
@ -20,8 +17,6 @@ class FriendlyMessageTime extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final statusIcon = _buildStatusIcon(Colors.grey.shade400);
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 6),
|
padding: const EdgeInsets.only(left: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -53,66 +48,10 @@ class FriendlyMessageTime extends StatelessWidget {
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
?statusIcon,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _buildStatusIcon(Color iconColor) {
|
|
||||||
if (message.type != MessageType.text.name || message.senderId != null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.ackByServer == null) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: ThreeRotatingDots(size: 8, color: iconColor),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.openedByAll != null || message.openedAt != null) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.solidEye,
|
|
||||||
size: 8,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now check message actions for ackByUserAt
|
|
||||||
return StreamBuilder<List<(MessageAction, Contact)>>(
|
|
||||||
stream: twonlyDB.messagesDao.watchMessageActions(message.messageId),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final actions = snapshot.data ?? [];
|
|
||||||
final hasAckByUser = actions.any(
|
|
||||||
(t) => t.$1.type == MessageActionType.ackByUserAt,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasAckByUser) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.checkDouble,
|
|
||||||
size: 8,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.check,
|
|
||||||
size: 8,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String friendlyTime(BuildContext context, DateTime dt) {
|
String friendlyTime(BuildContext context, DateTime dt) {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
|
||||||
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart';
|
|
||||||
import 'package:twonly/src/visual/views/chats/message_info.view.dart';
|
import 'package:twonly/src/visual/views/chats/message_info.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||||
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
||||||
|
|
||||||
class MessageContextMenu extends StatelessWidget {
|
class MessageContextMenu extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -426,7 +426,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
Future<void> _setupVideoPlayer(MediaFileService mediaLocal) async {
|
Future<void> _setupVideoPlayer(MediaFileService mediaLocal) async {
|
||||||
final controller = VideoPlayerController.file(
|
final controller = VideoPlayerController.file(
|
||||||
mediaLocal.tempPath,
|
mediaLocal.tempPath,
|
||||||
videoPlayerOptions: VideoPlayerOptions(),
|
videoPlayerOptions: VideoPlayerOptions(
|
||||||
|
mixWithOthers: mediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await controller.setLooping(
|
await controller.setLooping(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -10,9 +9,7 @@ import 'package:permission_handler/permission_handler.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/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|
||||||
import 'package:twonly/src/providers/routing.provider.dart';
|
import 'package:twonly/src/providers/routing.provider.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -50,14 +47,10 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
||||||
StreamSubscription<int>? _homeViewPageIndexSub;
|
StreamSubscription<int>? _homeViewPageIndexSub;
|
||||||
StreamSubscription<NotificationResponse>? _selectNotificationSub;
|
StreamSubscription<NotificationResponse>? _selectNotificationSub;
|
||||||
StreamSubscription<(String, MediaType)>? _sharedMediaSub;
|
|
||||||
|
|
||||||
static Uri? pendingSharedLink;
|
static Uri? pendingSharedLink;
|
||||||
static (String, MediaType)? pendingSharedMedia;
|
|
||||||
static final streamHomeViewPageIndex = StreamController<int>.broadcast();
|
static final streamHomeViewPageIndex = StreamController<int>.broadcast();
|
||||||
static final streamSharedLink = StreamController<Uri>.broadcast();
|
static final streamSharedLink = StreamController<Uri>.broadcast();
|
||||||
static final streamSharedMedia =
|
|
||||||
StreamController<(String, MediaType)>.broadcast();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -66,8 +59,6 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
var initialPage = widget.initialPage;
|
var initialPage = widget.initialPage;
|
||||||
if (HomeViewState.pendingSharedLink != null) {
|
if (HomeViewState.pendingSharedLink != null) {
|
||||||
initialPage = 1;
|
initialPage = 1;
|
||||||
} else if (HomeViewState.pendingSharedMedia != null) {
|
|
||||||
initialPage = 0;
|
|
||||||
} else if (initialPage == 1 &&
|
} else if (initialPage == 1 &&
|
||||||
!userService.currentUser.startWithCameraOpen) {
|
!userService.currentUser.startWithCameraOpen) {
|
||||||
initialPage = 0;
|
initialPage = 0;
|
||||||
|
|
@ -123,48 +114,12 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_sharedMediaSub = streamSharedMedia.stream.listen((media) async {
|
|
||||||
HomeViewState.pendingSharedMedia = null;
|
|
||||||
final type = media.$2;
|
|
||||||
final filePath = media.$1;
|
|
||||||
|
|
||||||
final newMediaService = await initializeMediaUpload(
|
|
||||||
type,
|
|
||||||
userService.currentUser.defaultShowTime,
|
|
||||||
);
|
|
||||||
if (newMediaService == null) {
|
|
||||||
Log.error('Could not create new media file for intent shared file');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final file = File(filePath);
|
|
||||||
if (!file.existsSync()) {
|
|
||||||
Log.error('The shared intent file does not exist.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file.copySync(newMediaService.originalPath.path);
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
await context.navPush(
|
|
||||||
ShareImageEditorView(
|
|
||||||
mediaFileService: newMediaService,
|
|
||||||
sharedFromGallery: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (HomeViewState.pendingSharedLink != null) {
|
if (HomeViewState.pendingSharedLink != null) {
|
||||||
final link = HomeViewState.pendingSharedLink!;
|
final link = HomeViewState.pendingSharedLink!;
|
||||||
HomeViewState.pendingSharedLink = null;
|
HomeViewState.pendingSharedLink = null;
|
||||||
_mainCameraController.setSharedLinkForPreview(link);
|
_mainCameraController.setSharedLinkForPreview(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HomeViewState.pendingSharedMedia != null) {
|
|
||||||
final media = HomeViewState.pendingSharedMedia!;
|
|
||||||
HomeViewState.pendingSharedMedia = null;
|
|
||||||
streamSharedMedia.add(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialPage == 1) {
|
if (initialPage == 1) {
|
||||||
Permission.camera.isGranted.then((hasPermission) {
|
Permission.camera.isGranted.then((hasPermission) {
|
||||||
if (hasPermission && mounted) {
|
if (hasPermission && mounted) {
|
||||||
|
|
@ -249,7 +204,6 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
_mainCameraController.setState = null;
|
_mainCameraController.setState = null;
|
||||||
_mainCameraController.closeCamera();
|
_mainCameraController.closeCamera();
|
||||||
_sharedLinkSub?.cancel();
|
_sharedLinkSub?.cancel();
|
||||||
_sharedMediaSub?.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
|
||||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
|
||||||
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
|
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
|
||||||
import 'package:twonly/src/services/subscription.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/elements/my_button.element.dart';
|
||||||
|
|
@ -18,9 +11,6 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isFreePlan =
|
|
||||||
context.watch<PurchasesProvider>().plan == SubscriptionPlan.Free;
|
|
||||||
|
|
||||||
return StreamBuilder<void>(
|
return StreamBuilder<void>(
|
||||||
stream: userService.onUserUpdated,
|
stream: userService.onUserUpdated,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|
@ -34,38 +24,33 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 8,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.color.surfaceContainerLow,
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderRadius: BorderRadius.circular(24),
|
gradient: LinearGradient(
|
||||||
boxShadow: [
|
colors: [
|
||||||
BoxShadow(
|
context.color.primaryContainer.withValues(alpha: 0.15),
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
context.color.primaryContainer.withValues(alpha: 0.05),
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: context.color.primary.withValues(alpha: 0.15),
|
color: context.color.primary.withValues(alpha: 0.1),
|
||||||
|
width: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
context.lang.memoriesBackupTitle,
|
context.lang.memoriesBackupTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -73,60 +58,21 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
||||||
color: context.color.onSurface,
|
color: context.color.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.amber.shade700,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const FaIcon(
|
|
||||||
FontAwesomeIcons.star,
|
|
||||||
size: 10,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
context.lang.backupCloudProBadge,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
Text(
|
||||||
context.lang.settingsStorageNoCloudBackupCard,
|
context.lang.settingsStorageNoCloudBackupCard,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: context.color.onSurfaceVariant,
|
color: context.color.onSurfaceVariant,
|
||||||
height: 1.4,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
MyButton(
|
MyButton(
|
||||||
variant: MyButtonVariant.primaryDense,
|
variant: MyButtonVariant.primaryDense,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (isFreePlan) {
|
|
||||||
unawaited(
|
|
||||||
context.push(Routes.settingsSubscription),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u.isCloudBackupEnabled = true,
|
(u) => u.isCloudBackupEnabled = true,
|
||||||
);
|
);
|
||||||
|
|
@ -134,7 +80,9 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: Text(context.lang.enable),
|
child: Text(context.lang.enable),
|
||||||
),
|
),
|
||||||
TextButton(
|
const SizedBox(width: 8),
|
||||||
|
MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryDense,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u.hideMemoriesBackupPromo = true,
|
(u) => u.hideMemoriesBackupPromo = true,
|
||||||
|
|
@ -142,10 +90,6 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.settingsStorageHidePromo,
|
context.lang.settingsStorageHidePromo,
|
||||||
style: TextStyle(
|
|
||||||
color: context.color.onSurfaceVariant,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -306,11 +306,11 @@ class MemoriesViewState extends State<MemoriesView>
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteCompletely == null || !mounted) return;
|
if (deleteCompletely == null) return;
|
||||||
final isCompletely = deleteCompletely;
|
final isCompletely = deleteCompletely;
|
||||||
|
|
||||||
await _showProgressDialog(
|
await _showProgressDialog(
|
||||||
context.lang.memoriesDeletingProgress,
|
'Deleting memories...',
|
||||||
(setProgress) async {
|
(setProgress) async {
|
||||||
for (var i = 0; i < selectedList.length; i++) {
|
for (var i = 0; i < selectedList.length; i++) {
|
||||||
final mediaId = selectedList[i];
|
final mediaId = selectedList[i];
|
||||||
|
|
@ -350,7 +350,7 @@ class MemoriesViewState extends State<MemoriesView>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _showProgressDialog(
|
await _showProgressDialog(
|
||||||
context.lang.memoriesExportingProgress,
|
'Exporting memories...',
|
||||||
(setProgress) async {
|
(setProgress) async {
|
||||||
for (var i = 0; i < selectedList.length; i++) {
|
for (var i = 0; i < selectedList.length; i++) {
|
||||||
final mediaId = selectedList[i];
|
final mediaId = selectedList[i];
|
||||||
|
|
@ -411,9 +411,7 @@ class MemoriesViewState extends State<MemoriesView>
|
||||||
final targetFav = !areAllFav;
|
final targetFav = !areAllFav;
|
||||||
|
|
||||||
await _showProgressDialog(
|
await _showProgressDialog(
|
||||||
targetFav
|
targetFav ? 'Adding to favorites...' : 'Removing from favorites...',
|
||||||
? context.lang.memoriesAddingToFavorites
|
|
||||||
: context.lang.memoriesRemovingFromFavorites,
|
|
||||||
(setProgress) async {
|
(setProgress) async {
|
||||||
for (var i = 0; i < selectedList.length; i++) {
|
for (var i = 0; i < selectedList.length; i++) {
|
||||||
final mediaId = selectedList[i];
|
final mediaId = selectedList[i];
|
||||||
|
|
|
||||||
|
|
@ -207,19 +207,6 @@ class _SynchronizedImageViewerScreenState
|
||||||
Future<void> _exportFile() async {
|
Future<void> _exportFile() async {
|
||||||
final item = widget.galleryItems[_currentIndex].mediaService;
|
final item = widget.galleryItems[_currentIndex].mediaService;
|
||||||
|
|
||||||
if (!item.storedPath.existsSync()) {
|
|
||||||
await item.storeMediaFile();
|
|
||||||
if (!mounted) return;
|
|
||||||
if (userService.currentUser.storeMediaFilesInGallery) {
|
|
||||||
showSnackbar(
|
|
||||||
context,
|
|
||||||
context.lang.galleryExportSuccess,
|
|
||||||
level: SnackbarLevel.success,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (item.mediaFile.type == MediaType.video) {
|
if (item.mediaFile.type == MediaType.video) {
|
||||||
await saveVideoToGallery(
|
await saveVideoToGallery(
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,6 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
final TextEditingController usernameCtrl = TextEditingController();
|
final TextEditingController usernameCtrl = TextEditingController();
|
||||||
final TextEditingController passwordCtrl = TextEditingController();
|
final TextEditingController passwordCtrl = TextEditingController();
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
usernameCtrl.dispose();
|
|
||||||
passwordCtrl.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _recoverTwonlySafe() async {
|
Future<void> _recoverTwonlySafe() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
||||||
Log.error('User ID already token. Tying again.');
|
Log.error('User ID already token. Tying again.');
|
||||||
await deleteLocalUserData();
|
await deleteLocalUserData();
|
||||||
return await createNewUser();
|
return createNewUser();
|
||||||
}
|
}
|
||||||
if (res.error == ErrorCode.UsernameAlreadyTaken ||
|
if (res.error == ErrorCode.UsernameAlreadyTaken ||
|
||||||
res.error == ErrorCode.UsernameNotValid) {
|
res.error == ErrorCode.UsernameNotValid) {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ 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/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/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart';
|
|
||||||
import 'package:twonly/src/visual/views/settings/backup/components/recovery_card.comp.dart';
|
|
||||||
import 'package:twonly/src/visual/views/settings/backup/memories_backup_detail.view.dart';
|
import 'package:twonly/src/visual/views/settings/backup/memories_backup_detail.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart';
|
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart';
|
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart';
|
||||||
|
|
@ -76,9 +74,6 @@ class _BackupViewState extends State<BackupView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||||
final isFreePlan = currentPlan == SubscriptionPlan.Free;
|
final isFreePlan = currentPlan == SubscriptionPlan.Free;
|
||||||
final hasPasswordless =
|
|
||||||
userService.currentUser.passwordLessRecovery != null;
|
|
||||||
final hasPassword = userService.currentUser.isBackupEnabled;
|
|
||||||
|
|
||||||
return StreamBuilder<void>(
|
return StreamBuilder<void>(
|
||||||
stream: userService.onUserUpdated,
|
stream: userService.onUserUpdated,
|
||||||
|
|
@ -91,33 +86,7 @@ class _BackupViewState extends State<BackupView> {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
// --- BEREICH 1: Konto-Wiederherstellung ---
|
if (userService.currentUser.passwordLessRecovery != null)
|
||||||
Text(
|
|
||||||
context.lang.backupRecoverySectionTitle,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Text(
|
|
||||||
(!hasPasswordless && !hasPassword)
|
|
||||||
? context.lang.backupRecoverySectionDescNone
|
|
||||||
: context.lang.backupRecoverySectionDescSome,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: (!hasPasswordless && !hasPassword)
|
|
||||||
? context.color.error
|
|
||||||
: context.color.onSurfaceVariant,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
if (hasPasswordless)
|
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: PasswordLessRecoveryStatus(),
|
child: PasswordLessRecoveryStatus(),
|
||||||
|
|
@ -125,46 +94,20 @@ class _BackupViewState extends State<BackupView> {
|
||||||
else
|
else
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: RecoveryCard(
|
child: Center(
|
||||||
icon: FontAwesomeIcons.shieldHeart,
|
child: MyButton(
|
||||||
title: context.lang.backupRecoveryOptionAFriends,
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
subtitle: context.lang.backupRecoveryOptionAMicrocopy,
|
onPressed: () =>
|
||||||
onTap: () =>
|
|
||||||
context.navPush(const PasswordLessRecoverySetup()),
|
context.navPush(const PasswordLessRecoverySetup()),
|
||||||
|
child: Text(context.lang.passwordlessRecoveryEnableBtn),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: RecoveryCard(
|
|
||||||
isEnabled: hasPassword,
|
|
||||||
icon: FontAwesomeIcons.key,
|
|
||||||
title: context.lang.backupRecoveryOptionBPassword,
|
|
||||||
subtitle: hasPassword
|
|
||||||
? context.lang.backupChangePassword
|
|
||||||
: context.lang.backupRecoveryOptionBMicrocopy,
|
|
||||||
onTap: () =>
|
|
||||||
context.push(Routes.settingsBackupSetup, extra: true),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// --- BEREICH 2: Cloud-Backup ---
|
|
||||||
Text(
|
|
||||||
context.lang.backupCloudSectionTitle,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.backupCloudSectionDesc,
|
context.lang.backupTwonlySafeDesc,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: context.color.onSurfaceVariant,
|
color: context.color.onSurfaceVariant,
|
||||||
|
|
@ -172,25 +115,39 @@ class _BackupViewState extends State<BackupView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 20),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// Kontakte & Nachrichten
|
if (userService.currentUser.isBackupEnabled) ...[
|
||||||
|
// 1. Identity tile
|
||||||
|
BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.userCheck,
|
||||||
|
text: context.lang.backupIdentityHeader,
|
||||||
|
subtitle: Text(
|
||||||
|
_buildTileSubtitle(
|
||||||
|
_backupStatus?.identityLastSuccessFull,
|
||||||
|
_backupStatus?.identitySize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 2. Contacts & Messages tile
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.comments,
|
icon: FontAwesomeIcons.comments,
|
||||||
text: context.lang.backupCloudContactsMessages,
|
text: context.lang.backupArchiveHeader,
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
_buildTileSubtitle(
|
_buildTileSubtitle(
|
||||||
_backupStatus?.archiveLastSuccessFull,
|
_backupStatus?.archiveLastSuccessFull,
|
||||||
_backupStatus?.archiveSize,
|
_backupStatus?.archiveSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.check_circle, color: Colors.green),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Bilder & Medien
|
// 3. Memories tile
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.photoFilm,
|
icon: FontAwesomeIcons.photoFilm,
|
||||||
text: context.lang.backupCloudImagesMedia,
|
text: context.lang.memoriesBackupTitle,
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
isFreePlan
|
isFreePlan
|
||||||
? context.lang.backupMemoriesUpgradeRequired
|
? context.lang.backupMemoriesUpgradeRequired
|
||||||
|
|
@ -200,83 +157,38 @@ class _BackupViewState extends State<BackupView> {
|
||||||
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
|
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
|
||||||
: '-')),
|
: '-')),
|
||||||
),
|
),
|
||||||
trailing: isFreePlan
|
trailing: Icon(
|
||||||
? Row(
|
Icons.chevron_right_rounded,
|
||||||
mainAxisSize: MainAxisSize.min,
|
color: context.color.onSurfaceVariant,
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
onTap: () async {
|
||||||
color: Colors.amber.shade700,
|
if (isFreePlan) {
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const FaIcon(
|
|
||||||
FontAwesomeIcons.star,
|
|
||||||
size: 10,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
context.lang.backupCloudProBadge,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Switch(
|
|
||||||
value: false,
|
|
||||||
onChanged: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Switch(
|
|
||||||
value: userService.currentUser.isCloudBackupEnabled,
|
|
||||||
onChanged: (val) async {
|
|
||||||
if (!val) {
|
|
||||||
final disabled =
|
|
||||||
await promptAndDisableMemoriesBackup(context);
|
|
||||||
if (disabled && mounted) setState(() {});
|
|
||||||
} else {
|
|
||||||
await UserService.update(
|
|
||||||
(u) => u.isCloudBackupEnabled = val,
|
|
||||||
);
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
unawaited(memoriesCloudService.checkUploads());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: isFreePlan
|
|
||||||
? () async {
|
|
||||||
await context.push(Routes.settingsSubscription);
|
await context.push(Routes.settingsSubscription);
|
||||||
}
|
} else if (!userService
|
||||||
: () async {
|
.currentUser
|
||||||
if (userService.currentUser.isCloudBackupEnabled) {
|
.isCloudBackupEnabled) {
|
||||||
await context.navPush(
|
|
||||||
const MemoriesBackupDetailView(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u.isCloudBackupEnabled = true,
|
(u) => u.isCloudBackupEnabled = true,
|
||||||
);
|
);
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
unawaited(memoriesCloudService.checkUploads());
|
unawaited(memoriesCloudService.checkUploads());
|
||||||
|
} else {
|
||||||
|
await context.navPush(const MemoriesBackupDetailView());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
Center(
|
Center(
|
||||||
child: MyButton(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (userService.currentUser.isBackupEnabled) ...[
|
||||||
|
MyButton(
|
||||||
variant: MyButtonVariant.secondaryDense,
|
variant: MyButtonVariant.secondaryDense,
|
||||||
onPressed: _isLoading
|
onPressed: _isLoading
|
||||||
? null
|
? null
|
||||||
|
|
@ -289,8 +201,23 @@ class _BackupViewState extends State<BackupView> {
|
||||||
},
|
},
|
||||||
child: Text(context.lang.backupTwonlySaveNow),
|
child: Text(context.lang.backupTwonlySaveNow),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
],
|
||||||
|
MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryDense,
|
||||||
|
onPressed: () => context.push(
|
||||||
|
Routes.settingsBackupSetup,
|
||||||
|
extra: true,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
!userService.currentUser.isBackupEnabled
|
||||||
|
? context.lang.backupEnableBackup
|
||||||
|
: context.lang.backupChangePassword,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
userService.currentUser.isBackupEnabled
|
userService.currentUser.isBackupEnabled
|
||||||
? context.lang.backupChangePassword
|
? context.lang.backupEnableBackup
|
||||||
: context.lang.backupEnableBackup,
|
: context.lang.backupChangePassword,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
import 'package:drift/drift.dart' hide Column;
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
|
||||||
|
|
||||||
Future<bool> promptAndDisableMemoriesBackup(BuildContext context) async {
|
|
||||||
final cloudOnlyCount =
|
|
||||||
await twonlyDB.mediaFilesDao.getCloudOnlyMemoriesCount();
|
|
||||||
|
|
||||||
if (!context.mounted) return false;
|
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => Dialog(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.lang.settingsStorageDisableBackupTitle,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: formattedText(
|
|
||||||
context,
|
|
||||||
context.lang.settingsStorageDisableBackupBody(
|
|
||||||
cloudOnlyCount,
|
|
||||||
),
|
|
||||||
textColor: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MyButton(
|
|
||||||
variant: MyButtonVariant.secondaryMiddle,
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
child: Text(context.lang.galleryCancel),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: MyButton(
|
|
||||||
variant: MyButtonVariant.errorMiddle,
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: Text(
|
|
||||||
context.lang.settingsStorageDisableBackupBtn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed == true) {
|
|
||||||
try {
|
|
||||||
await apiService.disableMemoriesBackup();
|
|
||||||
final allMedias = await (twonlyDB.select(
|
|
||||||
twonlyDB.mediaFiles,
|
|
||||||
)..where((t) => t.stored.equals(true))).get();
|
|
||||||
for (final media in allMedias) {
|
|
||||||
final ms = MediaFileService(media);
|
|
||||||
if (!ms.storedPath.existsSync()) {
|
|
||||||
ms.fullMediaRemoval();
|
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
|
||||||
const MediaFilesCompanion(
|
|
||||||
cloudState: Value(CloudState.none),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await UserService.update((u) => u.isCloudBackupEnabled = false);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
showSnackbar(context, e.toString());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
@ -22,9 +22,7 @@ class _MissingBackupCompState extends State<MissingBackupComp> {
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final user = userService.currentUser;
|
final user = userService.currentUser;
|
||||||
|
|
||||||
if (user.currentSetupPage != null ||
|
if (user.currentSetupPage != null || user.isBackupEnabled) {
|
||||||
user.isBackupEnabled ||
|
|
||||||
user.passwordLessRecovery != null) {
|
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
|
|
||||||
class RecoveryCard extends StatelessWidget {
|
|
||||||
const RecoveryCard({
|
|
||||||
required this.icon,
|
|
||||||
required this.title,
|
|
||||||
required this.subtitle,
|
|
||||||
required this.onTap,
|
|
||||||
super.key,
|
|
||||||
this.isEnabled = false,
|
|
||||||
});
|
|
||||||
final dynamic icon;
|
|
||||||
final String title;
|
|
||||||
final String subtitle;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final bool isEnabled;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final effectiveColor = isEnabled ? context.color.primary : Colors.red;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: context.color.surfaceContainer,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
onTap: onTap,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: effectiveColor.withValues(alpha: 0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: icon is IconData
|
|
||||||
? Icon(
|
|
||||||
icon as IconData,
|
|
||||||
color: effectiveColor,
|
|
||||||
size: 24,
|
|
||||||
)
|
|
||||||
: FaIcon(
|
|
||||||
icon as FaIconData?,
|
|
||||||
color: effectiveColor,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
subtitle,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.color.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 3),
|
|
||||||
Icon(
|
|
||||||
Icons.chevron_right_rounded,
|
|
||||||
color: context.color.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||||
as server;
|
as server;
|
||||||
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
|
import 'package:twonly/src/services/memories/memories_cloud.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/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/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart';
|
|
||||||
import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart';
|
import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart';
|
||||||
|
|
||||||
class MemoriesBackupDetailView extends StatefulWidget {
|
class MemoriesBackupDetailView extends StatefulWidget {
|
||||||
|
|
@ -20,6 +24,7 @@ class MemoriesBackupDetailView extends StatefulWidget {
|
||||||
|
|
||||||
class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
||||||
server.Response_MemoriesUsage? _memoriesUsage;
|
server.Response_MemoriesUsage? _memoriesUsage;
|
||||||
|
int _cloudOnlyCount = 0;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -30,19 +35,111 @@ class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
||||||
|
|
||||||
Future<void> _loadStats() async {
|
Future<void> _loadStats() async {
|
||||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||||
|
final cloudOnlyCount = await twonlyDB.mediaFilesDao
|
||||||
|
.getCloudOnlyMemoriesCount();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_memoriesUsage = memoriesUsage;
|
_memoriesUsage = memoriesUsage;
|
||||||
|
_cloudOnlyCount = cloudOnlyCount;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _disableBackup() async {
|
Future<void> _disableBackup() async {
|
||||||
final disabled = await promptAndDisableMemoriesBackup(context);
|
final confirmed = await showDialog<bool>(
|
||||||
if (disabled && mounted) {
|
context: context,
|
||||||
|
builder: (context) => Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.settingsStorageDisableBackupTitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: formattedText(
|
||||||
|
context,
|
||||||
|
context.lang.settingsStorageDisableBackupBody(
|
||||||
|
_cloudOnlyCount,
|
||||||
|
),
|
||||||
|
textColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryMiddle,
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: Text(context.lang.galleryCancel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.errorMiddle,
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: Text(
|
||||||
|
context.lang.settingsStorageDisableBackupBtn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
await apiService.disableMemoriesBackup();
|
||||||
|
final allMedias = await (twonlyDB.select(
|
||||||
|
twonlyDB.mediaFiles,
|
||||||
|
)..where((t) => t.stored.equals(true))).get();
|
||||||
|
for (final media in allMedias) {
|
||||||
|
final ms = MediaFileService(media);
|
||||||
|
if (!ms.storedPath.existsSync()) {
|
||||||
|
ms.fullMediaRemoval();
|
||||||
|
await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
||||||
|
const MediaFilesCompanion(
|
||||||
|
cloudState: Value(CloudState.none),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await UserService.update((u) => u.isCloudBackupEnabled = false);
|
||||||
|
if (mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(context, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,6 @@ class DataAndStorageView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DataAndStorageViewState extends State<DataAndStorageView> {
|
class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
late Future<Map<MediaType, int>> _storageStatsFuture;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_storageStatsFuture = twonlyDB.mediaFilesDao.getStorageStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> showAutoDownloadOptions(
|
Future<void> showAutoDownloadOptions(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ConnectivityResult connectionMode,
|
ConnectivityResult connectionMode,
|
||||||
|
|
@ -101,7 +93,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
FutureBuilder<Map<MediaType, int>>(
|
FutureBuilder<Map<MediaType, int>>(
|
||||||
future: _storageStatsFuture,
|
future: twonlyDB.mediaFilesDao.getStorageStats(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final stats = snapshot.data ?? {};
|
final stats = snapshot.data ?? {};
|
||||||
final totalBytes = stats.values.fold<int>(0, (a, b) => a + b);
|
final totalBytes = stats.values.fold<int>(0, (a, b) => a + b);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
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/components/cached_network_image.dart';
|
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.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/components/cached_network_image.dart';
|
|
||||||
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
|
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,10 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateUserAvatar(String svg) async {
|
Future<void> updateUserAvatar(String json, String svg) async {
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u
|
(u) => u
|
||||||
|
..avatarJson = json
|
||||||
..avatarSvg = svg
|
..avatarSvg = svg
|
||||||
..avatarCounter = u.avatarCounter + 1,
|
..avatarCounter = u.avatarCounter + 1,
|
||||||
);
|
);
|
||||||
|
|
@ -103,8 +104,9 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
|
|
||||||
Future<void> storeAvatarAndExit() async {
|
Future<void> storeAvatarAndExit() async {
|
||||||
await _avatarMakerController.saveAvatarSVG();
|
await _avatarMakerController.saveAvatarSVG();
|
||||||
|
final json = _avatarMakerController.getJsonOptionsSync();
|
||||||
final svg = _avatarMakerController.getAvatarSVGSync();
|
final svg = _avatarMakerController.getAvatarSVGSync();
|
||||||
await updateUserAvatar(svg);
|
await updateUserAvatar(json, svg);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
}
|
}
|
||||||
|
|
@ -116,8 +118,8 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
canPop: false,
|
canPop: false,
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
if (didPop) return;
|
if (didPop) return;
|
||||||
if (_avatarMakerController.getAvatarSVGSync() !=
|
if (_avatarMakerController.getJsonOptionsSync() !=
|
||||||
userService.currentUser.avatarSvg) {
|
userService.currentUser.avatarJson) {
|
||||||
// there where changes
|
// there where changes
|
||||||
final shouldPop = await _showBackDialog() ?? false;
|
final shouldPop = await _showBackDialog() ?? false;
|
||||||
if (context.mounted && shouldPop) {
|
if (context.mounted && shouldPop) {
|
||||||
|
|
|
||||||
234
pubspec.lock
234
pubspec.lock
|
|
@ -18,7 +18,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.68"
|
version: "1.3.68"
|
||||||
adaptive_number:
|
adaptive_number:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/adaptive_number"
|
path: "dependencies/adaptive_number"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
@ -91,17 +91,19 @@ packages:
|
||||||
audio_waveforms:
|
audio_waveforms:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/audio_waveforms"
|
name: audio_waveforms
|
||||||
relative: true
|
sha256: "03b3430ecf430a2e90185518a228c02be3d26653c62dd931e50d671213a6dbc8"
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
avatar_maker:
|
avatar_maker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/avatar_maker"
|
name: avatar_maker
|
||||||
relative: true
|
sha256: ca182e33343846427da68fc226325f630063da2de2f0bdb49e683f0c843b80c2
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "1.5.0"
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
background_downloader:
|
background_downloader:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -113,9 +115,10 @@ packages:
|
||||||
blurhash_dart:
|
blurhash_dart:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/blurhash_dart"
|
name: blurhash_dart
|
||||||
relative: true
|
sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f"
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|
@ -181,6 +184,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.5"
|
version: "8.12.5"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
camera:
|
camera:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -190,12 +217,14 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.0+1"
|
version: "0.12.0+1"
|
||||||
camera_android_camerax:
|
camera_android_camerax:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/camera_android_camerax"
|
path: "packages/camera/camera_android_camerax"
|
||||||
relative: true
|
ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||||
source: path
|
resolved-ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||||
version: "0.7.4+6"
|
url: "https://github.com/otsmr/flutter-packages.git"
|
||||||
|
source: git
|
||||||
|
version: "0.7.1+2"
|
||||||
camera_avfoundation:
|
camera_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -208,10 +237,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: camera_platform_interface
|
name: camera_platform_interface
|
||||||
sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498"
|
sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.1"
|
version: "2.12.0"
|
||||||
camera_web:
|
camera_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -381,7 +410,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.0"
|
version: "8.1.0"
|
||||||
dots_indicator:
|
dots_indicator:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/dots_indicator"
|
path: "dependencies/dots_indicator"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
@ -412,7 +441,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.3.0"
|
||||||
ed25519_edwards:
|
ed25519_edwards:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/ed25519_edwards"
|
path: "dependencies/ed25519_edwards"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
@ -599,6 +628,14 @@ packages:
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.9.1"
|
version: "0.9.1"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
flutter_driver:
|
flutter_driver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -949,7 +986,7 @@ packages:
|
||||||
source: path
|
source: path
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
hashlib_codecs:
|
hashlib_codecs:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/hashlib_codecs"
|
path: "dependencies/hashlib_codecs"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
@ -998,10 +1035,11 @@ packages:
|
||||||
image:
|
image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/image"
|
name: image
|
||||||
relative: true
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "4.9.2"
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1107,10 +1145,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.3"
|
version: "0.20.2"
|
||||||
introduction_screen:
|
introduction_screen:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1256,10 +1294,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.20"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1268,22 +1306,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.13.0"
|
||||||
material_symbols_icons:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: material_symbols_icons
|
|
||||||
sha256: bd513edc2bc9b034108d5518c48cf5cebbb67d0653728cc452368cb27ca80bb0
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.2960.0"
|
|
||||||
meta:
|
meta:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1331,8 +1361,16 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.3.0"
|
version: "9.3.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
optional:
|
optional:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/optional"
|
path: "dependencies/optional"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
@ -1514,7 +1552,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
pointycastle:
|
pointycastle:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/pointycastle"
|
path: "dependencies/pointycastle"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
@ -1539,10 +1577,11 @@ packages:
|
||||||
pro_video_editor:
|
pro_video_editor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/pro_video_editor"
|
name: pro_video_editor
|
||||||
relative: true
|
sha256: cfed1424b3ca3d5981cc81efdd20b844c995c0ad2818e185eb5bc06a8674f728
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "2.11.3"
|
source: hosted
|
||||||
|
version: "1.14.2"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1584,7 +1623,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
qr:
|
qr:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/qr"
|
path: "dependencies/qr"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
@ -1619,6 +1658,14 @@ packages:
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.28.0"
|
||||||
screen_protector:
|
screen_protector:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1768,12 +1815,52 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.2"
|
version: "1.10.2"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/sprintf"
|
path: "dependencies/sprintf"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "7.0.0"
|
version: "7.0.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+3"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
sqlcipher_flutter_libs:
|
sqlcipher_flutter_libs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1846,6 +1933,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1858,10 +1953,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.12"
|
version: "0.7.11"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1978,10 +2073,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.2.0"
|
||||||
very_good_analysis:
|
very_good_analysis:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1993,32 +2088,35 @@ packages:
|
||||||
video_player:
|
video_player:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/video_player"
|
name: video_player
|
||||||
relative: true
|
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "2.14.0"
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: "dependencies/video_player_android"
|
name: video_player_android
|
||||||
relative: true
|
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "2.12.0"
|
source: hosted
|
||||||
|
version: "2.9.5"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: "dependencies/video_player_avfoundation"
|
name: video_player_avfoundation
|
||||||
relative: true
|
sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "2.11.0"
|
source: hosted
|
||||||
|
version: "2.9.4"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_platform_interface
|
name: video_player_platform_interface
|
||||||
sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce"
|
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.9.0"
|
version: "6.6.0"
|
||||||
video_player_web:
|
video_player_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -2124,7 +2222,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.1+1"
|
version: "0.9.1+1"
|
||||||
x25519:
|
x25519:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "dependencies/x25519"
|
path: "dependencies/x25519"
|
||||||
relative: true
|
relative: true
|
||||||
|
|
|
||||||
167
pubspec.yaml
167
pubspec.yaml
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.5.2+172
|
version: 0.5.0+168
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
@ -37,6 +37,7 @@ dependencies:
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
vector_graphics: ^1.1.19
|
vector_graphics: ^1.1.19
|
||||||
|
video_player: ^2.10.1
|
||||||
in_app_purchase: ^3.3.0
|
in_app_purchase: ^3.3.0
|
||||||
go_router: ^17.1.0
|
go_router: ^17.1.0
|
||||||
|
|
||||||
|
|
@ -71,13 +72,34 @@ dependencies:
|
||||||
# Overwritten by self-controlled repository
|
# Overwritten by self-controlled repository
|
||||||
emoji_picker_flutter: ^4.3.0
|
emoji_picker_flutter: ^4.3.0
|
||||||
|
|
||||||
|
# Packages which got overwritten using the twonly-app-dependencies repository
|
||||||
|
# Idea: Every change goes though a git commit, where every change can be reviewed.
|
||||||
|
restart_app: ^1.3.2
|
||||||
|
photo_view: ^0.15.0
|
||||||
|
hashlib: ^2.0.0
|
||||||
|
libsignal_protocol_dart: ^0.7.4
|
||||||
|
lottie: ^3.3.1
|
||||||
|
mutex: ^3.1.0
|
||||||
|
introduction_screen: ^4.0.0
|
||||||
|
qr_flutter: ^4.1.0
|
||||||
|
hand_signature: ^3.0.3
|
||||||
|
flutter_sharing_intent: ^2.0.4
|
||||||
|
screen_protector: ^1.5.1
|
||||||
|
flutter_markdown_plus: ^1.0.7
|
||||||
|
exif: ^3.3.0
|
||||||
|
flutter_blurhash: ^0.9.1
|
||||||
|
|
||||||
# With high download. (But should be checked nonetheless.)
|
# With high download. (But should be checked nonetheless.)
|
||||||
app_links: ^7.0.0 # 1.6 mio
|
app_links: ^7.0.0 # 1.6 mio
|
||||||
|
image: ^4.3.0 # 3.3 mio
|
||||||
flutter_secure_storage: ^10.3.1 # 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
|
||||||
|
audio_waveforms: ^2.0.2
|
||||||
|
avatar_maker: ^0.4.0
|
||||||
background_downloader: ^9.4.0
|
background_downloader: ^9.4.0
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
cryptography_flutter_plus: ^3.0.0
|
cryptography_flutter_plus: ^3.0.0
|
||||||
cryptography_plus: ^3.0.0
|
cryptography_plus: ^3.0.0
|
||||||
flutter_android_volume_keydown: ^1.0.1
|
flutter_android_volume_keydown: ^1.0.1
|
||||||
|
|
@ -87,47 +109,60 @@ dependencies:
|
||||||
photo_manager: ^3.9.0
|
photo_manager: ^3.9.0
|
||||||
google_mlkit_barcode_scanning: ^0.14.1
|
google_mlkit_barcode_scanning: ^0.14.1
|
||||||
google_mlkit_face_detection: ^0.13.1
|
google_mlkit_face_detection: ^0.13.1
|
||||||
|
pro_video_editor: ^1.6.1
|
||||||
rust_lib_twonly:
|
rust_lib_twonly:
|
||||||
path: rust_builder
|
path: rust_builder
|
||||||
flutter_rust_bridge: 2.12.0
|
flutter_rust_bridge: 2.12.0
|
||||||
|
|
||||||
## --- Start Managed Dependencies ---
|
|
||||||
audio_waveforms: ^2.0.2
|
|
||||||
avatar_maker: ^1.5.0
|
|
||||||
blurhash_dart: ^1.2.1
|
blurhash_dart: ^1.2.1
|
||||||
exif: ^3.3.0
|
|
||||||
sprintf: ^7.0.0
|
|
||||||
flutter_blurhash: ^0.9.1
|
|
||||||
flutter_markdown_plus: ^1.0.7
|
|
||||||
video_player: ^2.14.0
|
|
||||||
video_player_android: ^2.12.0
|
|
||||||
video_player_avfoundation: ^2.11.0
|
|
||||||
camera_android_camerax: ^0.7.4+6
|
|
||||||
flutter_sharing_intent: ^2.0.4
|
|
||||||
hand_signature: ^3.1.0+2
|
|
||||||
hashlib: ^2.3.0
|
|
||||||
hashlib_codecs: ^3.0.1
|
|
||||||
image: ^4.9.2
|
|
||||||
introduction_screen: ^4.0.0
|
|
||||||
dots_indicator: ^4.0.1
|
|
||||||
libsignal_protocol_dart: ^0.8.0
|
|
||||||
adaptive_number: ^1.0.0
|
|
||||||
ed25519_edwards: ^0.3.1
|
|
||||||
optional: ^6.1.0+1
|
|
||||||
pointycastle: ^4.0.0
|
|
||||||
x25519: ^0.1.1
|
|
||||||
lottie: ^3.5.1
|
|
||||||
mutex: ^3.1.0
|
|
||||||
photo_view: ^0.15.0
|
|
||||||
pro_video_editor: ^2.11.3
|
|
||||||
qr_flutter: ^4.1.0
|
|
||||||
qr: ^3.1.0-wip
|
|
||||||
restart_app: ^1.7.3
|
|
||||||
screen_protector: ^1.5.1
|
|
||||||
## --- End Managed Dependencies ---
|
|
||||||
|
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
|
dots_indicator:
|
||||||
|
path: ./dependencies/dots_indicator
|
||||||
|
restart_app:
|
||||||
|
path: ./dependencies/restart_app
|
||||||
|
hashlib:
|
||||||
|
path: ./dependencies/hashlib
|
||||||
|
introduction_screen:
|
||||||
|
path: ./dependencies/introduction_screen
|
||||||
|
libsignal_protocol_dart:
|
||||||
|
path: ./dependencies/libsignal_protocol_dart
|
||||||
|
flutter_sharing_intent:
|
||||||
|
path: ./dependencies/flutter_sharing_intent
|
||||||
|
lottie:
|
||||||
|
path: ./dependencies/lottie
|
||||||
|
mutex:
|
||||||
|
path: ./dependencies/mutex
|
||||||
|
photo_view:
|
||||||
|
path: ./dependencies/photo_view
|
||||||
|
qr:
|
||||||
|
path: ./dependencies/qr
|
||||||
|
adaptive_number:
|
||||||
|
path: ./dependencies/adaptive_number
|
||||||
|
ed25519_edwards:
|
||||||
|
path: ./dependencies/ed25519_edwards
|
||||||
|
hand_signature:
|
||||||
|
path: ./dependencies/hand_signature
|
||||||
|
hashlib_codecs:
|
||||||
|
path: ./dependencies/hashlib_codecs
|
||||||
|
optional:
|
||||||
|
path: ./dependencies/optional
|
||||||
|
pointycastle:
|
||||||
|
path: ./dependencies/pointycastle
|
||||||
|
x25519:
|
||||||
|
path: ./dependencies/x25519
|
||||||
|
qr_flutter:
|
||||||
|
path: ./dependencies/qr_flutter
|
||||||
|
screen_protector:
|
||||||
|
path: ./dependencies/screen_protector
|
||||||
|
flutter_markdown_plus:
|
||||||
|
path: ./dependencies/flutter_markdown_plus
|
||||||
|
camera_android_camerax:
|
||||||
|
# path: ../flutter-packages/packages/camera/camera_android_camerax
|
||||||
|
git:
|
||||||
|
url: https://github.com/otsmr/flutter-packages.git
|
||||||
|
path: packages/camera/camera_android_camerax
|
||||||
|
ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||||
emoji_picker_flutter:
|
emoji_picker_flutter:
|
||||||
# Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238)
|
# Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238)
|
||||||
# Using override until this gets merged.
|
# Using override until this gets merged.
|
||||||
|
|
@ -138,74 +173,12 @@ dependency_overrides:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/yenchieh/flutter_android_volume_keydown.git
|
url: https://github.com/yenchieh/flutter_android_volume_keydown.git
|
||||||
ref: fix/lStar-not-found-error
|
ref: fix/lStar-not-found-error
|
||||||
|
|
||||||
## --- Start Managed Dependency Overrides ---
|
|
||||||
audio_waveforms:
|
|
||||||
path: ./dependencies/audio_waveforms
|
|
||||||
avatar_maker:
|
|
||||||
path: ./dependencies/avatar_maker
|
|
||||||
blurhash_dart:
|
|
||||||
path: ./dependencies/blurhash_dart
|
|
||||||
exif:
|
exif:
|
||||||
path: ./dependencies/exif
|
path: ./dependencies/exif
|
||||||
sprintf:
|
sprintf:
|
||||||
path: ./dependencies/sprintf
|
path: ./dependencies/sprintf
|
||||||
flutter_blurhash:
|
flutter_blurhash:
|
||||||
path: ./dependencies/flutter_blurhash
|
path: ./dependencies/flutter_blurhash
|
||||||
flutter_markdown_plus:
|
|
||||||
path: ./dependencies/flutter_markdown_plus
|
|
||||||
video_player:
|
|
||||||
path: ./dependencies/video_player
|
|
||||||
video_player_android:
|
|
||||||
path: ./dependencies/video_player_android
|
|
||||||
video_player_avfoundation:
|
|
||||||
path: ./dependencies/video_player_avfoundation
|
|
||||||
camera_android_camerax:
|
|
||||||
path: ./dependencies/camera_android_camerax
|
|
||||||
flutter_sharing_intent:
|
|
||||||
path: ./dependencies/flutter_sharing_intent
|
|
||||||
hand_signature:
|
|
||||||
path: ./dependencies/hand_signature
|
|
||||||
hashlib:
|
|
||||||
path: ./dependencies/hashlib
|
|
||||||
hashlib_codecs:
|
|
||||||
path: ./dependencies/hashlib_codecs
|
|
||||||
image:
|
|
||||||
path: ./dependencies/image
|
|
||||||
introduction_screen:
|
|
||||||
path: ./dependencies/introduction_screen
|
|
||||||
dots_indicator:
|
|
||||||
path: ./dependencies/dots_indicator
|
|
||||||
libsignal_protocol_dart:
|
|
||||||
path: ./dependencies/libsignal_protocol_dart
|
|
||||||
adaptive_number:
|
|
||||||
path: ./dependencies/adaptive_number
|
|
||||||
ed25519_edwards:
|
|
||||||
path: ./dependencies/ed25519_edwards
|
|
||||||
optional:
|
|
||||||
path: ./dependencies/optional
|
|
||||||
pointycastle:
|
|
||||||
path: ./dependencies/pointycastle
|
|
||||||
x25519:
|
|
||||||
path: ./dependencies/x25519
|
|
||||||
lottie:
|
|
||||||
path: ./dependencies/lottie
|
|
||||||
mutex:
|
|
||||||
path: ./dependencies/mutex
|
|
||||||
photo_view:
|
|
||||||
path: ./dependencies/photo_view
|
|
||||||
pro_video_editor:
|
|
||||||
path: ./dependencies/pro_video_editor
|
|
||||||
qr_flutter:
|
|
||||||
path: ./dependencies/qr_flutter
|
|
||||||
qr:
|
|
||||||
path: ./dependencies/qr
|
|
||||||
restart_app:
|
|
||||||
path: ./dependencies/restart_app
|
|
||||||
screen_protector:
|
|
||||||
path: ./dependencies/screen_protector
|
|
||||||
## --- End Managed Dependency Overrides ---
|
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.15
|
build_runner: ^2.4.15
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pro_video_editor/core/platform/io/io_helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parse_link.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parse_link.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ class UserEnvironment {
|
||||||
appVersion: 100,
|
appVersion: 100,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ignore: cascade_invocations
|
||||||
us.isUserCreated = true;
|
us.isUserCreated = true;
|
||||||
|
|
||||||
final identityKeyPair = generateIdentityKeyPair();
|
final identityKeyPair = generateIdentityKeyPair();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('BetterText parses URLs correctly', (tester) async {
|
testWidgets('BetterText parses URLs correctly', (WidgetTester tester) async {
|
||||||
const text =
|
const text =
|
||||||
'Test: (https://google.com) and another link https://example.com/#fragment, plus www.test.com. Also check https://wikipedia.org/wiki/Test_(disambiguation) !';
|
'Test: (https://google.com) and another link https://example.com/#fragment, plus www.test.com. Also check https://wikipedia.org/wiki/Test_(disambiguation) !';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue