mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:04:07 +00:00
Compare commits
21 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7412daf373 | |||
| 2ce240279f | |||
| ba09002d04 | |||
| 195f6091f2 | |||
| 39122476ce | |||
| b3f0324066 | |||
| 463ae81a51 | |||
| 25bc253b91 | |||
| 35133d2ebb | |||
| 2a6fd61a32 | |||
| 77d057c248 | |||
| 0c16188877 | |||
| 280519d63a | |||
| c13b80990f | |||
| 4b11031560 | |||
| 4792f5aac8 | |||
| 41e174becb | |||
| cf1cbb058e | |||
| 4183ebd8e6 | |||
| 1027856691 | |||
| eade1d87e6 |
70 changed files with 1889 additions and 837 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -56,3 +56,4 @@ devtools_options.yaml
|
|||
rust/target
|
||||
rust_dependencies/target
|
||||
fastlane/repo/status/running.json
|
||||
.cache/
|
||||
254
CHANGELOG.md
254
CHANGELOG.md
|
|
@ -1,5 +1,18 @@
|
|||
# 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
|
||||
|
||||
- New: Update to Signal's new PQC-ready key agreement PQXDH
|
||||
|
|
@ -49,8 +62,8 @@
|
|||
|
||||
## 0.3.0
|
||||
|
||||
- Improved: Design of some UI components
|
||||
- Improved: Memories viewer shows state for batch operations and has improved performance
|
||||
- Improve: Design of some UI components
|
||||
- Improve: Memories viewer shows state for batch operations and has improved performance
|
||||
- Fix: Issue with background notifications on Android
|
||||
- Fix: Changed minimum threshold for the user discovery to 3
|
||||
- Fix: Multiple UI issues
|
||||
|
|
@ -59,24 +72,24 @@
|
|||
## 0.2.26
|
||||
|
||||
- New: Import images from the gallery
|
||||
- Improved: Media files are now stored in the dedicated "twonly" album
|
||||
- Improved: UI components adapt to native styling (iOS/Android)
|
||||
- Fix: Migration issue that resulted in a corrupted backup mechanism
|
||||
- Improve: Media files are now stored in the dedicated "twonly" album
|
||||
- Improve: UI components adapt to native styling (iOS/Android)
|
||||
- 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: Permission view did not disappear after they were granted
|
||||
|
||||
## 0.2.23
|
||||
|
||||
- Improved: Smaller UI changes
|
||||
- Improve: Smaller UI changes
|
||||
- Fix: Some messages were not marked as opened.
|
||||
|
||||
## 0.2.20
|
||||
|
||||
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
||||
- New: Adds security profiles.
|
||||
- Improved: Onboarding flow for new users.
|
||||
- Improved: Flame restore experience.
|
||||
- Improved: The blue verification checkmark now displays the total number of verifications.
|
||||
- Improve: Onboarding flow for new users.
|
||||
- Improve: Flame restore experience.
|
||||
- Improve: The blue verification checkmark now displays the total number of verifications.
|
||||
- Fix: Issue with receiving messages when user closed app while decrypting
|
||||
- Fix: Background message fetching reliability.
|
||||
- Fix: Issue with focus changing when taking a picture
|
||||
|
|
@ -93,36 +106,36 @@
|
|||
|
||||
## 0.2.13
|
||||
|
||||
- New: Tutorial on how to use zoom.
|
||||
- New: Tutorial on how to use zoom.
|
||||
- New: Manage storage view.
|
||||
- Improved: Media thumbnails for faster loading.
|
||||
- Improve: Media thumbnails for faster loading.
|
||||
- Fix: Some messages were not marked as opened.
|
||||
|
||||
## 0.2.12
|
||||
|
||||
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
|
||||
- Improved: Memories viewer redesigned with smoother animations and new quick-action controls.
|
||||
- Improve: Memories viewer redesigned with smoother animations and new quick-action controls.
|
||||
- Fix: Reliability of receiving media files.
|
||||
|
||||
## 0.2.11
|
||||
|
||||
- New: Create custom shortcuts to quickly share images with pre-selected groups
|
||||
- New: Seamless recovery for iOS reinstallations
|
||||
- Improved: Redesigned snackbar notifications
|
||||
- Improved: New backup mechanism to allow larger backup files
|
||||
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
|
||||
- Improve: Redesigned snackbar notifications
|
||||
- Improve: New backup mechanism to allow larger backup files
|
||||
- Improve: Move keys into a centralized Rust-owned structure stored in secure storage
|
||||
- Fix: Messages occasionally not received until app restart
|
||||
- Fix: Multiple smaller issues
|
||||
|
||||
## 0.2.10
|
||||
|
||||
- Fix: Issue with push notifications on Android
|
||||
- Fix: Issue with push notifications on Android
|
||||
|
||||
## 0.2.9
|
||||
|
||||
- Improved: Make contact avatars clickable
|
||||
- Improve: Make contact avatars clickable
|
||||
- Fix: Messages occasionally not received until app restart
|
||||
- Fix: Complete setup would sometimes get stuck
|
||||
- Fix: Complete setup would sometimes get stuck
|
||||
|
||||
## 0.2.8
|
||||
|
||||
|
|
@ -133,29 +146,29 @@
|
|||
- New: Feature to find friends without a phone number
|
||||
- New: The verification state is now transferred to the scanned user
|
||||
- New: Registration setup to configure the most important configurations
|
||||
- Improved: Show ⌛ instead of the flame icon when it is about to expire
|
||||
- Improved: FAQ is now in the app rather than opening in the browser
|
||||
- Improved: Videos can now be paused
|
||||
- Improved: Lock to record hands-free
|
||||
- Improve: 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
|
||||
- Improve: Videos can now be paused
|
||||
- Improve: Lock to record hands-free
|
||||
- Fix: Many smaller issues
|
||||
|
||||
## 0.1.8
|
||||
|
||||
- Improved: Typos and grammar issues thanks to @AlbertUnruh
|
||||
- Improve: Typos and grammar issues thanks to @AlbertUnruh
|
||||
- Fix: App becomes unresponsive when clicking notifications.
|
||||
|
||||
## 0.1.7
|
||||
|
||||
- Improved: Show input indicator in the chat overview as well
|
||||
- Improved: Username change error handling
|
||||
- Fix: Phantom push notification
|
||||
- Improve: Show input indicator in the chat overview as well
|
||||
- Improve: Username change error handling
|
||||
- Fix: Phantom push notification
|
||||
- Fix: Start in chat, if configured
|
||||
- Fix: Smaller UI fixes
|
||||
|
||||
## 0.1.5
|
||||
|
||||
- Fix: Reupload of media files was not working properly
|
||||
- Fix: Chats were sometimes ordered wrongly
|
||||
- Fix: Chats were sometimes ordered wrongly
|
||||
- Fix: Typing indicator was not always shown
|
||||
- Fix: Multiple smaller issues
|
||||
|
||||
|
|
@ -163,7 +176,7 @@
|
|||
|
||||
- New: Typing and chat open indicator
|
||||
- New: Screen lock for twonly (Can be enabled in the settings.)
|
||||
- Improve: Visual indication when connected to the server
|
||||
- Improve: Visual indication when connected to the server
|
||||
- Improve: Several minor issues with the user interface
|
||||
- Fix: Poor audio quality and edge distortions in videos sent from Android
|
||||
|
||||
|
|
@ -173,23 +186,23 @@
|
|||
- New: Crop or rotate images before sharing them.
|
||||
- New: Clicking on “Text Notifications” will now open the chat directly (Android only)
|
||||
- New: Developer settings to reduce flames
|
||||
- Improve: Improved troubleshooting for issues with push notifications
|
||||
- Improve: Improved troubleshooting for issues with push notifications
|
||||
- Improve: A message appears if someone has deleted their account.
|
||||
- Improve: Make the verification badge more visible.
|
||||
- Fix: Flash not activated when starting a video recording
|
||||
- Fix: Problem sending media when a recipient has deleted their account.
|
||||
- Fix: Receive push notifications without receiving an in-app message (Android)
|
||||
- Fix: Issue with sending GIFs from Memories
|
||||
- Fix: Incorrect processing of messages that have already been fetched from the server causes the UI to freeze
|
||||
- Fix: Incorrect processing of messages that have already been fetched from the server causes the UI to freeze
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- New: Groups can now collect flames as well
|
||||
- New: Background execution to pre-load messages
|
||||
- New: Background execution to pre-load messages
|
||||
- New: Adds a link if the image contains a QR code
|
||||
- Improve: Video compression with progress updates
|
||||
- Improve: Show message "Flames restored"
|
||||
- Improve: Show toast message if user was added via QR
|
||||
- Improve: Show toast message if user was added via QR
|
||||
- Fix: Media file appears as a white square and is not listed.
|
||||
- Fix: Issue with media files required to be reuploaded
|
||||
- Fix: Problem during contact requests
|
||||
|
|
@ -199,17 +212,17 @@
|
|||
|
||||
## 0.0.96
|
||||
|
||||
- Feature: Show link in chat if the saved media file contains one
|
||||
- Improve: Verification badge for groups
|
||||
- New: Show link in chat if the saved media file contains one
|
||||
- Improve: Verification badge for groups
|
||||
- Improve: Huge reduction in app size
|
||||
- Fix: Crash on older devices when compressing a video
|
||||
- Fix: Problem with decrypting messages fixed
|
||||
|
||||
## 0.0.93
|
||||
|
||||
- Feature: Verification checkmark for friends
|
||||
- New: Verification checkmark for friends
|
||||
- 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: Route not found when sharing image
|
||||
- Fix: Increase recent limit in emoji keyboard
|
||||
|
|
@ -220,139 +233,138 @@
|
|||
|
||||
## 0.0.92
|
||||
|
||||
- Adds the option to share contacts
|
||||
- Adds option to zoom in received images / videos
|
||||
- Fixes issue with "reuploaded requested" not working
|
||||
- Fixes race condition while writing to the log file
|
||||
- New: The option to share contacts
|
||||
- New: Option to zoom in received images / videos
|
||||
- Fix: Issue with "reuploaded requested" not working
|
||||
- Fix: Race condition while writing to the log file
|
||||
|
||||
## 0.0.91
|
||||
|
||||
- Fixes link preview on iOS
|
||||
- Fixes sharing images from other apps on iOS
|
||||
- Fix: Link preview on iOS
|
||||
- Fix: Sharing images from other apps on iOS
|
||||
|
||||
## 0.0.90
|
||||
|
||||
- Fixes issue that media files where not reuploaded
|
||||
- Fixes iOS zooming issue when switching between .5 and x1
|
||||
- Fixes biometric auth bypass when opening a twonly/reopen send image
|
||||
- Fixes that media files could not be downloaded in case the contact deleted his account
|
||||
- Fixes database issue in case twonly is opened multiple times
|
||||
- Fixes typos in translation
|
||||
- Fix: Issue that media files were not reuploaded
|
||||
- Fix: iOS zooming issue when switching between .5 and x1
|
||||
- Fix: 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
|
||||
- Fix: Database issue in case twonly is opened multiple times
|
||||
- Fix: Typos in translation
|
||||
|
||||
## 0.0.87
|
||||
|
||||
- Adds link preview to shared links
|
||||
- Adds option to manual focus in the camera
|
||||
- Adds support to switch between front and back cameras during video recording
|
||||
- Adds basic face filters
|
||||
- Improves image editor, like emojis or text under a drawing can be moved
|
||||
- Improves speed after taking a picture
|
||||
- Fixes issue with emojis disappearing in the image editor
|
||||
- New: Link preview to shared links
|
||||
- New: Option to manual focus in the camera
|
||||
- New: Support to switch between front and back cameras during video recording
|
||||
- New: Basic face filters
|
||||
- Improve: Image editor, like emojis or text under a drawing can be moved
|
||||
- Improve: Speed after taking a picture
|
||||
- Fix: Issue with emojis disappearing in the image editor
|
||||
|
||||
## 0.0.86
|
||||
|
||||
- Allows to reopen send images (if send without time limit or enabled auth)
|
||||
- Added support for front camera zoom
|
||||
- Several bug fixes
|
||||
- New: Allows to reopen send images (if send without time limit or enabled auth)
|
||||
- New: Support for front camera zoom
|
||||
- Fix: Several bug fixes
|
||||
|
||||
## 0.0.83
|
||||
|
||||
- Improved view of the diagnostic log
|
||||
- Several bug fixes
|
||||
- Improve: View of the diagnostic log
|
||||
- Fix: Several bug fixes
|
||||
|
||||
## 0.0.82
|
||||
|
||||
- Added an option in the settings to automatically save all sent images
|
||||
- Hides duplicate images in the memory
|
||||
- Fixes a bug where messages were not being received
|
||||
- Several other minor improvements
|
||||
- New: An option in the settings to automatically save all sent images
|
||||
- New: Hides duplicate images in the memory
|
||||
- Improve: Several other minor improvements
|
||||
- Fix: A bug where messages were not being received
|
||||
|
||||
## 0.0.81
|
||||
|
||||
- Fixes the issue where black/blank images were sometimes received
|
||||
- Fixes an issue in the image editor
|
||||
- Fix: The issue where black/blank images were sometimes received
|
||||
- Fix: An issue in the image editor
|
||||
|
||||
## 0.0.80
|
||||
|
||||
- Share images/videos directly from other applications
|
||||
- More customization options in the appearance settings
|
||||
- Improved UI for changing the display time of images
|
||||
- Several minor UI improvements
|
||||
- Several bug fixes
|
||||
- New: Share images/videos directly from other applications
|
||||
- New: More customization options in the appearance settings
|
||||
- Improve: UI for changing the display time of images
|
||||
- Improve: Several minor UI improvements
|
||||
- Fix: Several bug fixes
|
||||
|
||||
## 0.0.74
|
||||
|
||||
- Improving uploading speed
|
||||
- Fixing issue with ffmpeg for android
|
||||
- Improve: Uploading speed
|
||||
- Fix: Issue with ffmpeg for android
|
||||
|
||||
## 0.0.73
|
||||
|
||||
- Integrated QR code scanner in the main camera
|
||||
- New profile share page
|
||||
- New workflow for checking the security number
|
||||
- Improved user interface for creating voice messages
|
||||
- New: Integrated QR code scanner in the main camera
|
||||
- New: Profile share page
|
||||
- New: Workflow for checking the security number
|
||||
- Improve: User interface for creating voice messages
|
||||
|
||||
## 0.0.69
|
||||
|
||||
- Option to export and import memories
|
||||
- iOS support for ultra-wide-angle camera
|
||||
- Support Android Monochrome Icon
|
||||
- Multiple layout issues fixed
|
||||
- Multiple bug fixes
|
||||
- New: Option to export and import memories
|
||||
- New: iOS support for ultra-wide-angle camera
|
||||
- New: Support Android Monochrome Icon
|
||||
- Fix: Multiple layout issues fixed
|
||||
- Fix: Multiple bug fixes
|
||||
|
||||
## 0.0.67
|
||||
|
||||
- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
|
||||
- Fixes bug when saving images to the gallery
|
||||
- Multiple layout issues fixed
|
||||
- Multiple bug fixes
|
||||
- New: Crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
|
||||
- Fix: Bug when saving images to the gallery
|
||||
- Fix: Multiple layout issues fixed
|
||||
- Fix: Multiple bug fixes
|
||||
|
||||
## 0.0.62
|
||||
|
||||
- Support for groups with multiple administrators
|
||||
- Edit and delete messages
|
||||
- Create images using volume buttons
|
||||
- New and improved emoji picker
|
||||
- Removing audio after recording is possible
|
||||
- Edited image is now embedded into the video
|
||||
- Video max length increased to 60 seconds
|
||||
- Switched to FFmpeg for improved video compression
|
||||
- New context menu and other UI enhancements
|
||||
- Client-to-client protocol migrated to Protocol Buffers (Protobuf)
|
||||
- Database identifiers converted to UUIDs and the database schema completely redesigned
|
||||
- Improved reliability of client-to-client messaging
|
||||
- Multiple bug fixes
|
||||
- New: Support for groups with multiple administrators
|
||||
- New: Edit and delete messages
|
||||
- New: Create images using volume buttons
|
||||
- New: Removing audio after recording is possible
|
||||
- New: Edited image is now embedded into the video
|
||||
- New: Video max length increased to 60 seconds
|
||||
- New: Context menu and other UI enhancements
|
||||
- New: Client-to-client protocol migrated to Protocol Buffers (Protobuf)
|
||||
- New: Database identifiers converted to UUIDs and the database schema completely redesigned
|
||||
- Improve: Emoji picker
|
||||
- Improve: Switched to FFmpeg for improved video compression
|
||||
- Improve: Reliability of client-to-client messaging
|
||||
- Fix: Multiple bug fixes
|
||||
|
||||
## 0.0.61
|
||||
|
||||
- Improving image editor when changing colors
|
||||
- Fixing message decryption error
|
||||
- Fixing issue with user deletion
|
||||
- Fixing issue with flame counter sync
|
||||
- Dependency and Flutter upgrade
|
||||
- New: Image editor when changing colors
|
||||
- New: Dependency and Flutter upgrade
|
||||
- Fix: Message decryption error
|
||||
- Fix: Issue with user deletion
|
||||
- Fix: Issue with flame counter sync
|
||||
|
||||
## 0.0.60
|
||||
|
||||
- Improved logging to debug the 'Tap to load' issue.
|
||||
- Display your own avatar in the title bar of the chat list.
|
||||
- Created a default avatar image in case none was set.
|
||||
- Improved UI handling when requesting microphone access for the first time.
|
||||
- Flutter SDK and dependencies upgraded.
|
||||
- Multiple bug fixes.
|
||||
- New: Display your own avatar in the title bar of the chat list.
|
||||
- New: Created a default avatar image in case none was set.
|
||||
- New: Flutter SDK and dependencies upgraded.
|
||||
- Improve: Logging to debug the 'Tap to load' issue.
|
||||
- Improve: UI handling when requesting microphone access for the first time.
|
||||
- Fix: Multiple bug fixes.
|
||||
|
||||
## 0.0.59
|
||||
|
||||
- Fixing media download error
|
||||
- Fixing issue with video recording
|
||||
- Location Filter are now stored as WebP instead of PNG
|
||||
- New: Location Filter are now stored as WebP instead of PNG
|
||||
- Fix: Media download error
|
||||
- Fix: Issue with video recording
|
||||
|
||||
## 0.0.58
|
||||
|
||||
- twonly now has a free plan and is now financed by donations and an optional subscription with more features (coming soon)
|
||||
- iOS gestures to close images
|
||||
- Improved chat messages view, including better citation view and display times
|
||||
- Onboarding screens updated and registration view simplified
|
||||
- The sender is displayed in the top right corner when a media file is opened
|
||||
- Images are now stored as WebP to save storage
|
||||
- Button to report users
|
||||
- Multiple bug fixes
|
||||
- New: iOS gestures to close images
|
||||
- New: Onboarding screens updated and registration view simplified
|
||||
- New: The sender is displayed in the top right corner when a media file is opened
|
||||
- New: Images are now stored as WebP to save storage
|
||||
- New: Button to report users
|
||||
- Improve: Chat messages view, including better citation view and display times
|
||||
- Fix: Multiple bug fixes
|
||||
|
|
|
|||
15
README.md
15
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
|
||||
|
||||
- Offer a Snapchat™ like experience
|
||||
- End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll)
|
||||
- End-to-End encryption using the [Signal Protocol](https://signal.org/docs/specifications/pqxdh/)
|
||||
- Open Source and can be downloaded directly from GitHub
|
||||
- No email or phone number required to register
|
||||
- The backend is hosted exclusively in Europe
|
||||
|
|
@ -40,19 +40,12 @@ 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
|
||||
- E2EE cloud backup of memories
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Currently
|
||||
|
||||
- Implementation of features so that Snapchat can actually be replaced
|
||||
- Importing memories from Snapchat
|
||||
|
||||
### Next on the bucket list
|
||||
## Next on the bucket list
|
||||
|
||||
- Importing memories from Snapchat
|
||||
- For Android: Support for [UnifiedPush] (https://unifiedpush.org/)
|
||||
- For Android: Reproducible builds
|
||||
- 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
|
||||
|
||||
## Security Issues
|
||||
|
|
@ -63,7 +56,7 @@ guarantee a bounty currently :/
|
|||
|
||||
## Contribution
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Signing Keys
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ analyzer:
|
|||
- "**.arb"
|
||||
- "test/drift/**"
|
||||
- "**.g.dart"
|
||||
- build/**
|
||||
- android/**
|
||||
- ios/**
|
||||
- web/**
|
||||
- windows/**
|
||||
- macos/**
|
||||
- linux/**
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ android {
|
|||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
manifestPlaceholders = [appName: "twonly"]
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
|
|
@ -53,6 +54,7 @@ android {
|
|||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".testing"
|
||||
manifestPlaceholders = [appName: "twonly [dev]"]
|
||||
}
|
||||
// profile {
|
||||
// applicationIdSuffix ".STOP"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<application
|
||||
android:label="twonly"
|
||||
android:label="${appName}"
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 32882a31904a0c1ffd38974bf5158ceee0f3ecb7
|
||||
Subproject commit b874b9cc309822d90b940fdfbc8fd1ad3afc2450
|
||||
204
dependencies.py
Normal file
204
dependencies.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
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()
|
||||
109
dependencies.yaml
Normal file
109
dependencies.yaml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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,8 +92,6 @@ PODS:
|
|||
- nanopb/encode (3.30910.0)
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- pro_video_editor (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- rust_lib_twonly (0.0.1):
|
||||
- Flutter
|
||||
|
|
@ -122,7 +120,6 @@ DEPENDENCIES:
|
|||
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
||||
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/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`)
|
||||
- screen_protector (from `.symlinks/plugins/screen_protector/ios`)
|
||||
- SwiftProtobuf
|
||||
|
|
@ -170,8 +167,6 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
pro_video_editor:
|
||||
:path: ".symlinks/plugins/pro_video_editor/ios"
|
||||
rust_lib_twonly:
|
||||
:path: ".symlinks/plugins/rust_lib_twonly/ios"
|
||||
screen_protector:
|
||||
|
|
@ -182,7 +177,7 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
||||
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
|
||||
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
||||
|
|
@ -203,7 +198,6 @@ SPEC CHECKSUMS:
|
|||
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
||||
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
||||
|
|
|
|||
|
|
@ -742,7 +742,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
@ -886,7 +886,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
|
@ -939,7 +939,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.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/model/json/onboarding_state.model.dart';
|
||||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
|
|
@ -180,9 +181,17 @@ 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(
|
||||
context,
|
||||
handleShareLink,
|
||||
handleShareMedia,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
),
|
||||
],
|
||||
)..where(groups.isDirectChat.equals(false)));
|
||||
return query.map((row) => row.readTable(groupMembers)).get();
|
||||
return await query.map((row) => row.readTable(groupMembers)).get();
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return [];
|
||||
|
|
@ -346,8 +346,9 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
DateTime newLastMessage,
|
||||
) async {
|
||||
final now = clock.now();
|
||||
final clampedLastMessage =
|
||||
newLastMessage.isAfter(now) ? now : newLastMessage;
|
||||
final clampedLastMessage = newLastMessage.isAfter(now)
|
||||
? now
|
||||
: newLastMessage;
|
||||
await (update(groupMembers)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
await (delete(receivedReceipts)..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
const Duration(days: 45),
|
||||
),
|
||||
)),
|
||||
))
|
||||
|
|
@ -250,6 +250,25 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
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 {
|
||||
await into(
|
||||
receivedReceipts,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|||
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
|
||||
|
||||
@DataClassName('Message')
|
||||
@TableIndex(name: 'idx_messages_group_id_created_at', columns: {#groupId, #createdAt})
|
||||
class Messages extends Table {
|
||||
TextColumn get groupId =>
|
||||
text().references(Groups, #groupId, onDelete: KeyAction.cascade)();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
|
|||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
|
||||
@DataClassName('Receipt')
|
||||
@TableIndex(name: 'idx_receipts_message_id', columns: {#messageId})
|
||||
class Receipts extends Table {
|
||||
TextColumn get receiptId => text()();
|
||||
|
||||
|
|
|
|||
|
|
@ -13843,6 +13843,14 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
);
|
||||
late final $LabelsTable labels = $LabelsTable(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 ContactsDao contactsDao = ContactsDao(this as TwonlyDB);
|
||||
late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB);
|
||||
|
|
@ -13889,6 +13897,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
shortcutMembers,
|
||||
labels,
|
||||
contactLabels,
|
||||
idxMessagesGroupIdCreatedAt,
|
||||
idxReceiptsMessageId,
|
||||
];
|
||||
@override
|
||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
|
||||
|
|
|
|||
|
|
@ -1601,7 +1601,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @backupSelectStrongPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose a secure password. This is required if you want to restore your backup.'**
|
||||
/// **'Select a secure password. It is recommended to generate and store it via your password manager of choice.'**
|
||||
String get backupSelectStrongPassword;
|
||||
|
||||
/// No description provided for @password.
|
||||
|
|
@ -1631,7 +1631,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @backupEnableBackup.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Activate automatic backup'**
|
||||
/// **'Enable Password Backup'**
|
||||
String get backupEnableBackup;
|
||||
|
||||
/// No description provided for @backupTwonlySaveNow.
|
||||
|
|
@ -3713,7 +3713,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecovery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password Recovery'**
|
||||
/// **'Trusted Friends'**
|
||||
String get passwordlessRecovery;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNotConfigured.
|
||||
|
|
@ -3833,7 +3833,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecoveryEnableSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Passwordless recovery successfully enabled!'**
|
||||
/// **'Trusted Friends successfully set up!'**
|
||||
String get passwordlessRecoveryEnableSuccess;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryEnterPin.
|
||||
|
|
@ -3851,19 +3851,19 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecoveryEnableBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable Passwordless Recovery'**
|
||||
/// **'Enable Trusted Friends'**
|
||||
String get passwordlessRecoveryEnableBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryRecoverBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recover passwordless'**
|
||||
/// **'Recover via Trusted Friends'**
|
||||
String get passwordlessRecoveryRecoverBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryModifyBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Modify Passwordless Recovery'**
|
||||
/// **'Modify Trusted Friends'**
|
||||
String get passwordlessRecoveryModifyBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryStatusEnabled.
|
||||
|
|
@ -4435,6 +4435,108 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Dark'**
|
||||
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
|
||||
|
|
|
|||
|
|
@ -850,7 +850,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupSelectStrongPassword =>
|
||||
'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein Backup wiederherstellen möchtest.';
|
||||
'Wähle ein sicheres Passwort. Es wird empfohlen, dieses über deinen bevorzugten Passwort-Manager zu generieren und zu speichern.';
|
||||
|
||||
@override
|
||||
String get password => 'Passwort';
|
||||
|
|
@ -866,7 +866,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
||||
|
||||
@override
|
||||
String get backupEnableBackup => 'Automatische Sicherung aktivieren';
|
||||
String get backupEnableBackup => 'Passwort-Backup aktivieren';
|
||||
|
||||
@override
|
||||
String get backupTwonlySaveNow => 'Jetzt speichern';
|
||||
|
|
@ -2144,7 +2144,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Passwort vergessen';
|
||||
String get passwordlessRecovery => 'Vertraute Freunde';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
||||
|
|
@ -2210,7 +2210,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableSuccess =>
|
||||
'\"Passwort vergessen\" erfolgreich aktiviert!';
|
||||
'Vertraute Freunde erfolgreich eingerichtet!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
||||
|
|
@ -2220,16 +2220,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Bitte gib eine E-Mail-Adresse ein.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableBtn =>
|
||||
'\"Passwort vergessen\" aktivieren';
|
||||
String get passwordlessRecoveryEnableBtn => 'Vertraute Freunde aktivieren';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryRecoverBtn =>
|
||||
'Mit \"Passwort vergessen\" wiederherstellen';
|
||||
'Mithilfe von vertrauten Freunden wiederherstellen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyBtn =>
|
||||
'\"Passwort vergessen\" bearbeiten';
|
||||
String get passwordlessRecoveryModifyBtn => 'Vertraute Freunde bearbeiten';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
|
|
@ -2568,4 +2566,60 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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
|
||||
String get backupSelectStrongPassword =>
|
||||
'Choose a secure password. This is required if you want to restore your backup.';
|
||||
'Select a secure password. It is recommended to generate and store it via your password manager of choice.';
|
||||
|
||||
@override
|
||||
String get password => 'Password';
|
||||
|
|
@ -861,7 +861,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
'Password must be at least 10 characters long.';
|
||||
|
||||
@override
|
||||
String get backupEnableBackup => 'Activate automatic backup';
|
||||
String get backupEnableBackup => 'Enable Password Backup';
|
||||
|
||||
@override
|
||||
String get backupTwonlySaveNow => 'Save now';
|
||||
|
|
@ -2129,7 +2129,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get avatarCustomizeReset => 'Reset';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Password Recovery';
|
||||
String get passwordlessRecovery => 'Trusted Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
||||
|
|
@ -2195,7 +2195,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableSuccess =>
|
||||
'Passwordless recovery successfully enabled!';
|
||||
'Trusted Friends successfully set up!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterPin => 'Please enter a PIN.';
|
||||
|
|
@ -2204,13 +2204,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get passwordlessRecoveryEnterEmail => 'Please enter an email address.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery';
|
||||
String get passwordlessRecoveryEnableBtn => 'Enable Trusted Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryRecoverBtn => 'Recover passwordless';
|
||||
String get passwordlessRecoveryRecoverBtn => 'Recover via Trusted Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery';
|
||||
String get passwordlessRecoveryModifyBtn => 'Modify Trusted Friends';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
|
|
@ -2541,4 +2541,60 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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 ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5
|
||||
Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486
|
||||
|
|
@ -23,7 +23,6 @@ class UserData {
|
|||
String username;
|
||||
String displayName;
|
||||
String? avatarSvg;
|
||||
String? avatarJson;
|
||||
|
||||
@JsonKey(defaultValue: 0)
|
||||
int appVersion = 0;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
|
||||
)
|
||||
..avatarSvg = json['avatarSvg'] as String?
|
||||
..avatarJson = json['avatarJson'] as String?
|
||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
||||
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
||||
|
|
@ -129,7 +128,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'username': instance.username,
|
||||
'displayName': instance.displayName,
|
||||
'avatarSvg': instance.avatarSvg,
|
||||
'avatarJson': instance.avatarJson,
|
||||
'appVersion': instance.appVersion,
|
||||
'avatarCounter': instance.avatarCounter,
|
||||
'isDeveloper': instance.isDeveloper,
|
||||
|
|
|
|||
|
|
@ -299,9 +299,17 @@ class ApiService {
|
|||
return result;
|
||||
}
|
||||
|
||||
Future<void> sendResponse(ClientToServer response) async {
|
||||
if (_channel != null) {
|
||||
_channel!.sink.add(response.writeToBuffer());
|
||||
Future<bool> sendResponse(ClientToServer response) async {
|
||||
final channel = _channel;
|
||||
if (channel == null || channel.closeCode != null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
channel.sink.add(response.writeToBuffer());
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.warn('Could not send response to server: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -227,11 +227,19 @@ Future<void> finishStartedPreprocessing() async {
|
|||
mediaFile.mediaId,
|
||||
);
|
||||
if (messages.isEmpty) {
|
||||
Log.info(
|
||||
'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.',
|
||||
);
|
||||
MediaFileService(mediaFile).fullMediaRemoval();
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
||||
if (mediaFile.createdAt.isBefore(
|
||||
clock.now().subtract(const Duration(hours: 1)),
|
||||
)) {
|
||||
Log.info(
|
||||
'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.',
|
||||
);
|
||||
MediaFileService(mediaFile).fullMediaRemoval();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -484,12 +492,20 @@ Future<void> _startBackgroundMediaUploadInternal(
|
|||
mediaService.mediaFile.mediaId,
|
||||
);
|
||||
if (messages.isEmpty) {
|
||||
Log.warn(
|
||||
'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(
|
||||
mediaService.mediaFile.mediaId,
|
||||
);
|
||||
if (mediaService.mediaFile.createdAt.isBefore(
|
||||
clock.now().subtract(const Duration(hours: 1)),
|
||||
)) {
|
||||
Log.warn(
|
||||
'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(
|
||||
mediaService.mediaFile.mediaId,
|
||||
);
|
||||
} else {
|
||||
Log.warn(
|
||||
'Media files ${mediaService.mediaFile.mediaId} has no paths, but is too new to be deleted. Skipping deletion.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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.',
|
||||
|
|
|
|||
|
|
@ -114,22 +114,6 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
// ignore: parameter_assignments
|
||||
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(
|
||||
receipt.contactId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -80,9 +80,16 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
|||
..seq = msg.v0.seq
|
||||
..response = response;
|
||||
|
||||
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||
final responseSent = 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;
|
||||
Log.info('All messages from the server proccessed.');
|
||||
Log.info('All messages from the server processed.');
|
||||
}
|
||||
|
||||
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
||||
|
|
@ -141,6 +148,43 @@ Future<void> _handleClient2ClientMessage(
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -269,12 +313,13 @@ Future<void> _handleClient2ClientMessage(
|
|||
} catch (e) {
|
||||
Log.warn('[$receiptId] Error inserting receipt: $e');
|
||||
}
|
||||
if (targetReceiptId != null) {
|
||||
await tryToSendCompleteMessage(
|
||||
receiptId: targetReceiptId,
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
|
||||
targetReceiptId ??= receiptIdDB?.value ?? receiptId;
|
||||
|
||||
await tryToSendCompleteMessage(
|
||||
receiptId: targetReceiptId,
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
case Message_Type.TEST_NOTIFICATION:
|
||||
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
|
||||
// function again, to sync the local database
|
||||
return fetchGroupState(group);
|
||||
return await fetchGroupState(group);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -12,7 +10,6 @@ import 'package:twonly/locator.dart';
|
|||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.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'
|
||||
show PasswordlessRecoveryService;
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
|
|
@ -20,7 +17,6 @@ import 'package:twonly/src/utils/log.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.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_new_contact.view.dart';
|
||||
|
||||
|
|
@ -149,53 +145,20 @@ 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(
|
||||
BuildContext context,
|
||||
void Function(Uri) onUrlCallBack,
|
||||
void Function(String, MediaType) onMediaCallBack,
|
||||
) {
|
||||
FlutterSharingIntent.instance.getInitialSharing().then((f) {
|
||||
if (!context.mounted) return;
|
||||
handleIntentSharedFile(context, f, onUrlCallBack);
|
||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
});
|
||||
|
||||
return FlutterSharingIntent.instance.getMediaStream().listen(
|
||||
(f) {
|
||||
if (!context.mounted) return;
|
||||
handleIntentSharedFile(context, f, onUrlCallBack);
|
||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
},
|
||||
// ignore: inference_failure_on_untyped_parameter
|
||||
onError: (err) {
|
||||
|
|
@ -208,6 +171,7 @@ Future<void> handleIntentSharedFile(
|
|||
BuildContext context,
|
||||
List<SharedFile> files,
|
||||
void Function(Uri) onUrlCallBack,
|
||||
void Function(String, MediaType) onMediaCallBack,
|
||||
) async {
|
||||
for (final file in files) {
|
||||
if (file.value == null) {
|
||||
|
|
@ -231,9 +195,9 @@ Future<void> handleIntentSharedFile(
|
|||
if (file.value!.endsWith('.gif')) {
|
||||
type = MediaType.gif;
|
||||
}
|
||||
await handleIntentMediaFile(context, file.value!, type);
|
||||
onMediaCallBack(file.value!, type);
|
||||
case SharedMediaType.VIDEO:
|
||||
await handleIntentMediaFile(context, file.value!, MediaType.video);
|
||||
onMediaCallBack(file.value!, MediaType.video);
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,11 +202,11 @@ class PasswordlessRecoveryService {
|
|||
case SecondFactorType.pin:
|
||||
|
||||
// The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin
|
||||
config.serverKeyProtection = getRandomUint8List(32);
|
||||
|
||||
// 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.
|
||||
config.pinUnlockToken = getRandomUint8List(32);
|
||||
config
|
||||
..serverKeyProtection = getRandomUint8List(32)
|
||||
// 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.
|
||||
..pinUnlockToken = getRandomUint8List(32);
|
||||
|
||||
// Brute-force protection for the user's pin:
|
||||
// - Server: Does not know the seed.
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@ signalDecryptMessageV1(
|
|||
) = await lockingSignalProtocol.protect(() async {
|
||||
Log.info('Lock acquired for $fromUserId (V1)');
|
||||
try {
|
||||
// Yield execution to the event loop to prevent UI freezing during bulk decryption
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
final session = SessionCipher.fromStore(
|
||||
(await getSignalStore())!,
|
||||
getSignalAddress(fromUserId),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'dart:ui' as ui;
|
|||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -114,29 +115,33 @@ File currentUserAvatarFile(int avatarCounter) {
|
|||
return File('${avatarsDirectory.path}/user_$avatarCounter.png');
|
||||
}
|
||||
|
||||
final _avatarMutex = Mutex();
|
||||
|
||||
Future<String?> getUserAvatar() async {
|
||||
if (userService.currentUser.avatarSvg == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final avatarCounter = userService.currentUser.avatarCounter;
|
||||
final file = currentUserAvatarFile(avatarCounter);
|
||||
if (file.existsSync()) {
|
||||
return _avatarMutex.protect(() async {
|
||||
final avatarCounter = userService.currentUser.avatarCounter;
|
||||
final file = currentUserAvatarFile(avatarCounter);
|
||||
if (file.existsSync()) {
|
||||
return file.path;
|
||||
}
|
||||
|
||||
final pictureInfo = await vg.loadPicture(
|
||||
SvgStringLoader(userService.currentUser.avatarSvg!),
|
||||
null,
|
||||
);
|
||||
|
||||
final image = await pictureInfo.picture.toImage(270, 300);
|
||||
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
await file.writeAsBytes(pngBytes, flush: true);
|
||||
pictureInfo.picture.dispose();
|
||||
|
||||
return file.path;
|
||||
}
|
||||
|
||||
final pictureInfo = await vg.loadPicture(
|
||||
SvgStringLoader(userService.currentUser.avatarSvg!),
|
||||
null,
|
||||
);
|
||||
|
||||
final image = await pictureInfo.picture.toImage(270, 300);
|
||||
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
await file.writeAsBytes(pngBytes);
|
||||
pictureInfo.picture.dispose();
|
||||
|
||||
return file.path;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
|
|
@ -6,7 +7,6 @@ import 'package:convert/convert.dart';
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -44,41 +44,41 @@ Future<String?> saveImageToGallery(
|
|||
|
||||
if (createdAt != null) {
|
||||
try {
|
||||
final image = img.decodeImage(imageBytes);
|
||||
if (image != null) {
|
||||
final formattedDate = DateFormat(
|
||||
'yyyy:MM:dd HH:mm:ss',
|
||||
).format(createdAt);
|
||||
image.exif.imageIfd[0x0132] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTime
|
||||
image.exif.exifIfd[0x9003] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeOriginal
|
||||
image.exif.exifIfd[0x9004] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeDigitized
|
||||
bytesToProcess = await Isolate.run(() {
|
||||
final image = img.decodeImage(imageBytes);
|
||||
if (image != null) {
|
||||
final formattedDate = DateFormat(
|
||||
'yyyy:MM:dd HH:mm:ss',
|
||||
).format(createdAt);
|
||||
image.exif.imageIfd[0x0132] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTime
|
||||
image.exif.exifIfd[0x9003] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeOriginal
|
||||
image.exif.exifIfd[0x9004] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeDigitized
|
||||
|
||||
bytesToProcess = img.encodeJpg(image);
|
||||
}
|
||||
return img.encodeJpg(image);
|
||||
}
|
||||
return imageBytes;
|
||||
});
|
||||
} catch (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);
|
||||
if (!hasAccess) {
|
||||
await Gal.requestAccess(toAlbum: true);
|
||||
}
|
||||
try {
|
||||
await Gal.putImageBytes(jpgImages, album: 'twonly', name: name ?? 'image');
|
||||
await Gal.putImageBytes(
|
||||
bytesToProcess,
|
||||
album: 'twonly',
|
||||
name: name ?? 'image',
|
||||
);
|
||||
return null;
|
||||
} on GalException catch (e) {
|
||||
Log.error(e);
|
||||
|
|
|
|||
148
lib/src/visual/components/cached_network_image.dart
Normal file
148
lib/src/visual/components/cached_network_image.dart
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
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,9 +24,7 @@ class _VideoPlayerFileHelperState extends State<VideoPlayerFileHelper> {
|
|||
super.initState();
|
||||
_controller = VideoPlayerController.file(
|
||||
widget.videoPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: true,
|
||||
),
|
||||
videoPlayerOptions: VideoPlayerOptions(),
|
||||
);
|
||||
|
||||
unawaited(
|
||||
|
|
|
|||
|
|
@ -780,10 +780,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
mc.sharedLinkForPreview != null &&
|
||||
mc.sharedLinkForPreview!.shouldGeneratePreview &&
|
||||
!mc.isVideoRecording)
|
||||
ShowTitleText(
|
||||
title: mc.sharedLinkForPreview!.url.host,
|
||||
desc: 'Link',
|
||||
isLink: true,
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Chip(
|
||||
label: Text(mc.sharedLinkForPreview!.url.host),
|
||||
onDeleted: () {
|
||||
mc.setSharedLinkForPreview(null);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!mc.isSharePreviewIsShown &&
|
||||
!mc.isVideoRecording &&
|
||||
|
|
|
|||
|
|
@ -91,10 +91,10 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||
),
|
||||
)
|
||||
else if (_imageSaved)
|
||||
const Icon(Icons.check, size: 14)
|
||||
else
|
||||
_imageSaved
|
||||
? const Icon(Icons.check, size: 14)
|
||||
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 14),
|
||||
const FaIcon(FontAwesomeIcons.floppyDisk, size: 14),
|
||||
if (widget.displayButtonLabel) const SizedBox(width: 10),
|
||||
if (widget.displayButtonLabel)
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -116,9 +116,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
});
|
||||
videoController = VideoPlayerController.file(
|
||||
mediaService.originalPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: true,
|
||||
),
|
||||
videoPlayerOptions: VideoPlayerOptions(),
|
||||
);
|
||||
videoController?.setLooping(true);
|
||||
videoController
|
||||
|
|
@ -127,7 +125,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
await videoController!.play();
|
||||
setState(() {});
|
||||
})
|
||||
// ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error
|
||||
// ignore: argument_type_not_assignable_to_error_handler
|
||||
.catchError(Log.error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:cached_network_image/cached_network_image.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';
|
||||
|
||||
class ImageFilter extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.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';
|
||||
|
||||
class CustomLinkCard extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/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:font_awesome_flutter/font_awesome_flutter.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
|
||||
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:font_awesome_flutter/font_awesome_flutter.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';
|
||||
|
||||
class YouTubePostCard extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
contactSub?.cancel();
|
||||
groupActionsSub?.cancel();
|
||||
_nextTypingIndicator?.cancel();
|
||||
try {
|
||||
textFieldFocus?.dispose();
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.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/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';
|
||||
|
|
@ -202,7 +203,22 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
)
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: right
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
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(
|
||||
msg: widget.message,
|
||||
group: widget.group,
|
||||
|
|
@ -256,17 +272,18 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (!right && !widget.group.isDirectChat)
|
||||
hideContactAvatar
|
||||
? const SizedBox(width: 24)
|
||||
: GestureDetector(
|
||||
onTap: () => context.push(
|
||||
Routes.profileContact(widget.message.senderId!),
|
||||
),
|
||||
child: AvatarIcon(
|
||||
contactId: widget.message.senderId,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (hideContactAvatar)
|
||||
const SizedBox(width: 24)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () => context.push(
|
||||
Routes.profileContact(widget.message.senderId!),
|
||||
),
|
||||
child: AvatarIcon(
|
||||
contactId: widget.message.senderId,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -51,18 +51,6 @@ class ChatTextEntry extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import 'package:clock/clock.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
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/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||
|
||||
class FriendlyMessageTime extends StatelessWidget {
|
||||
const FriendlyMessageTime({
|
||||
|
|
@ -17,6 +20,8 @@ class FriendlyMessageTime extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statusIcon = _buildStatusIcon(Colors.grey.shade400);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Row(
|
||||
|
|
@ -48,10 +53,66 @@ class FriendlyMessageTime extends StatelessWidget {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -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/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/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/chats/message_info.view.dart';
|
||||
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -426,9 +426,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
Future<void> _setupVideoPlayer(MediaFileService mediaLocal) async {
|
||||
final controller = VideoPlayerController.file(
|
||||
mediaLocal.tempPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: mediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||
),
|
||||
videoPlayerOptions: VideoPlayerOptions(),
|
||||
);
|
||||
|
||||
await controller.setLooping(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -9,7 +10,9 @@ import 'package:permission_handler/permission_handler.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.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/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -47,10 +50,14 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
||||
StreamSubscription<int>? _homeViewPageIndexSub;
|
||||
StreamSubscription<NotificationResponse>? _selectNotificationSub;
|
||||
StreamSubscription<(String, MediaType)>? _sharedMediaSub;
|
||||
|
||||
static Uri? pendingSharedLink;
|
||||
static (String, MediaType)? pendingSharedMedia;
|
||||
static final streamHomeViewPageIndex = StreamController<int>.broadcast();
|
||||
static final streamSharedLink = StreamController<Uri>.broadcast();
|
||||
static final streamSharedMedia =
|
||||
StreamController<(String, MediaType)>.broadcast();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -59,6 +66,8 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
var initialPage = widget.initialPage;
|
||||
if (HomeViewState.pendingSharedLink != null) {
|
||||
initialPage = 1;
|
||||
} else if (HomeViewState.pendingSharedMedia != null) {
|
||||
initialPage = 0;
|
||||
} else if (initialPage == 1 &&
|
||||
!userService.currentUser.startWithCameraOpen) {
|
||||
initialPage = 0;
|
||||
|
|
@ -114,12 +123,48 @@ 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) {
|
||||
final link = HomeViewState.pendingSharedLink!;
|
||||
HomeViewState.pendingSharedLink = null;
|
||||
_mainCameraController.setSharedLinkForPreview(link);
|
||||
}
|
||||
|
||||
if (HomeViewState.pendingSharedMedia != null) {
|
||||
final media = HomeViewState.pendingSharedMedia!;
|
||||
HomeViewState.pendingSharedMedia = null;
|
||||
streamSharedMedia.add(media);
|
||||
}
|
||||
|
||||
if (initialPage == 1) {
|
||||
Permission.camera.isGranted.then((hasPermission) {
|
||||
if (hasPermission && mounted) {
|
||||
|
|
@ -204,6 +249,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
_mainCameraController.setState = null;
|
||||
_mainCameraController.closeCamera();
|
||||
_sharedLinkSub?.cancel();
|
||||
_sharedMediaSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/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/subscription.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
|
@ -11,6 +18,9 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFreePlan =
|
||||
context.watch<PurchasesProvider>().plan == SubscriptionPlan.Free;
|
||||
|
||||
return StreamBuilder<void>(
|
||||
stream: userService.onUserUpdated,
|
||||
builder: (context, snapshot) {
|
||||
|
|
@ -24,55 +34,99 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.color.primaryContainer.withValues(alpha: 0.15),
|
||||
context.color.primaryContainer.withValues(alpha: 0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
color: context.color.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: context.color.primary.withValues(alpha: 0.1),
|
||||
width: 1.5,
|
||||
color: context.color.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.memoriesBackupTitle,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: context.color.onSurface,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.lang.memoriesBackupTitle,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: context.color.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
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: 4),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
context.lang.settingsStorageNoCloudBackupCard,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.color.onSurfaceVariant,
|
||||
height: 1.3,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: () async {
|
||||
if (isFreePlan) {
|
||||
unawaited(
|
||||
context.push(Routes.settingsSubscription),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await UserService.update(
|
||||
(u) => u.isCloudBackupEnabled = true,
|
||||
);
|
||||
|
|
@ -80,9 +134,7 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
},
|
||||
child: Text(context.lang.enable),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await UserService.update(
|
||||
(u) => u.hideMemoriesBackupPromo = true,
|
||||
|
|
@ -90,6 +142,10 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
},
|
||||
child: Text(
|
||||
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 (deleteCompletely == null) return;
|
||||
if (deleteCompletely == null || !mounted) return;
|
||||
final isCompletely = deleteCompletely;
|
||||
|
||||
await _showProgressDialog(
|
||||
'Deleting memories...',
|
||||
context.lang.memoriesDeletingProgress,
|
||||
(setProgress) async {
|
||||
for (var i = 0; i < selectedList.length; i++) {
|
||||
final mediaId = selectedList[i];
|
||||
|
|
@ -350,7 +350,7 @@ class MemoriesViewState extends State<MemoriesView>
|
|||
|
||||
try {
|
||||
await _showProgressDialog(
|
||||
'Exporting memories...',
|
||||
context.lang.memoriesExportingProgress,
|
||||
(setProgress) async {
|
||||
for (var i = 0; i < selectedList.length; i++) {
|
||||
final mediaId = selectedList[i];
|
||||
|
|
@ -411,7 +411,9 @@ class MemoriesViewState extends State<MemoriesView>
|
|||
final targetFav = !areAllFav;
|
||||
|
||||
await _showProgressDialog(
|
||||
targetFav ? 'Adding to favorites...' : 'Removing from favorites...',
|
||||
targetFav
|
||||
? context.lang.memoriesAddingToFavorites
|
||||
: context.lang.memoriesRemovingFromFavorites,
|
||||
(setProgress) async {
|
||||
for (var i = 0; i < selectedList.length; i++) {
|
||||
final mediaId = selectedList[i];
|
||||
|
|
|
|||
|
|
@ -207,6 +207,19 @@ class _SynchronizedImageViewerScreenState
|
|||
Future<void> _exportFile() async {
|
||||
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 {
|
||||
if (item.mediaFile.type == MediaType.video) {
|
||||
await saveVideoToGallery(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
final TextEditingController usernameCtrl = TextEditingController();
|
||||
final TextEditingController passwordCtrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
usernameCtrl.dispose();
|
||||
passwordCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _recoverTwonlySafe() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
||||
Log.error('User ID already token. Tying again.');
|
||||
await deleteLocalUserData();
|
||||
return createNewUser();
|
||||
return await createNewUser();
|
||||
}
|
||||
if (res.error == ErrorCode.UsernameAlreadyTaken ||
|
||||
res.error == ErrorCode.UsernameNotValid) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import 'package:twonly/src/services/user.service.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/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/passwordless_recovery/components/status.passwordless_recovery.comp.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart';
|
||||
|
|
@ -74,6 +76,9 @@ class _BackupViewState extends State<BackupView> {
|
|||
Widget build(BuildContext context) {
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
final isFreePlan = currentPlan == SubscriptionPlan.Free;
|
||||
final hasPasswordless =
|
||||
userService.currentUser.passwordLessRecovery != null;
|
||||
final hasPassword = userService.currentUser.isBackupEnabled;
|
||||
|
||||
return StreamBuilder<void>(
|
||||
stream: userService.onUserUpdated,
|
||||
|
|
@ -86,7 +91,33 @@ class _BackupViewState extends State<BackupView> {
|
|||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ListView(
|
||||
children: [
|
||||
if (userService.currentUser.passwordLessRecovery != null)
|
||||
// --- BEREICH 1: Konto-Wiederherstellung ---
|
||||
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(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: PasswordLessRecoveryStatus(),
|
||||
|
|
@ -94,20 +125,46 @@ class _BackupViewState extends State<BackupView> {
|
|||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Center(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: () =>
|
||||
context.navPush(const PasswordLessRecoverySetup()),
|
||||
child: Text(context.lang.passwordlessRecoveryEnableBtn),
|
||||
),
|
||||
child: RecoveryCard(
|
||||
icon: FontAwesomeIcons.shieldHeart,
|
||||
title: context.lang.backupRecoveryOptionAFriends,
|
||||
subtitle: context.lang.backupRecoveryOptionAMicrocopy,
|
||||
onTap: () =>
|
||||
context.navPush(const PasswordLessRecoverySetup()),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
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: 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: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
context.lang.backupTwonlySafeDesc,
|
||||
context.lang.backupCloudSectionDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
|
|
@ -115,109 +172,125 @@ class _BackupViewState extends State<BackupView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (userService.currentUser.isBackupEnabled) ...[
|
||||
// 1. Identity tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.userCheck,
|
||||
text: context.lang.backupIdentityHeader,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.identityLastSuccessFull,
|
||||
_backupStatus?.identitySize,
|
||||
),
|
||||
// Kontakte & Nachrichten
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.comments,
|
||||
text: context.lang.backupCloudContactsMessages,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.archiveLastSuccessFull,
|
||||
_backupStatus?.archiveSize,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.check_circle, color: Colors.green),
|
||||
),
|
||||
|
||||
// 2. Contacts & Messages tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.comments,
|
||||
text: context.lang.backupArchiveHeader,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.archiveLastSuccessFull,
|
||||
_backupStatus?.archiveSize,
|
||||
),
|
||||
),
|
||||
// Bilder & Medien
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.photoFilm,
|
||||
text: context.lang.backupCloudImagesMedia,
|
||||
subtitle: Text(
|
||||
isFreePlan
|
||||
? context.lang.backupMemoriesUpgradeRequired
|
||||
: (!userService.currentUser.isCloudBackupEnabled
|
||||
? context.lang.backupMemoriesNotEnabled
|
||||
: (_memoriesUsage != null
|
||||
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
|
||||
: '-')),
|
||||
),
|
||||
trailing: isFreePlan
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.shade700,
|
||||
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);
|
||||
}
|
||||
: () async {
|
||||
if (userService.currentUser.isCloudBackupEnabled) {
|
||||
await context.navPush(
|
||||
const MemoriesBackupDetailView(),
|
||||
);
|
||||
} else {
|
||||
await UserService.update(
|
||||
(u) => u.isCloudBackupEnabled = true,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
unawaited(memoriesCloudService.checkUploads());
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// 3. Memories tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.photoFilm,
|
||||
text: context.lang.memoriesBackupTitle,
|
||||
subtitle: Text(
|
||||
isFreePlan
|
||||
? context.lang.backupMemoriesUpgradeRequired
|
||||
: (!userService.currentUser.isCloudBackupEnabled
|
||||
? context.lang.backupMemoriesNotEnabled
|
||||
: (_memoriesUsage != null
|
||||
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
|
||||
: '-')),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: context.color.onSurfaceVariant,
|
||||
),
|
||||
onTap: () async {
|
||||
if (isFreePlan) {
|
||||
await context.push(Routes.settingsSubscription);
|
||||
} else if (!userService
|
||||
.currentUser
|
||||
.isCloudBackupEnabled) {
|
||||
await UserService.update(
|
||||
(u) => u.isCloudBackupEnabled = true,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
unawaited(memoriesCloudService.checkUploads());
|
||||
} else {
|
||||
await context.navPush(const MemoriesBackupDetailView());
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (userService.currentUser.isBackupEnabled) ...[
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
await BackupService.makeBackup(force: true);
|
||||
await _loadBackupStatus();
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
await BackupService.makeBackup(force: true);
|
||||
await _loadBackupStatus();
|
||||
},
|
||||
child: Text(context.lang.backupTwonlySaveNow),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
|||
const SizedBox(width: 8),
|
||||
Text(
|
||||
userService.currentUser.isBackupEnabled
|
||||
? context.lang.backupEnableBackup
|
||||
: context.lang.backupChangePassword,
|
||||
? context.lang.backupChangePassword
|
||||
: context.lang.backupEnableBackup,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
111
lib/src/visual/views/settings/backup/backup_utils.dart
Normal file
111
lib/src/visual/views/settings/backup/backup_utils.dart
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
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,7 +22,9 @@ class _MissingBackupCompState extends State<MissingBackupComp> {
|
|||
builder: (context, snapshot) {
|
||||
final user = userService.currentUser;
|
||||
|
||||
if (user.currentSetupPage != null || user.isBackupEnabled) {
|
||||
if (user.currentSetupPage != null ||
|
||||
user.isBackupEnabled ||
|
||||
user.passwordLessRecovery != null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
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,17 +1,13 @@
|
|||
import 'dart:async';
|
||||
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/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||
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/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';
|
||||
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';
|
||||
|
||||
class MemoriesBackupDetailView extends StatefulWidget {
|
||||
|
|
@ -24,7 +20,6 @@ class MemoriesBackupDetailView extends StatefulWidget {
|
|||
|
||||
class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
||||
server.Response_MemoriesUsage? _memoriesUsage;
|
||||
int _cloudOnlyCount = 0;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
|
|
@ -35,110 +30,18 @@ class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
|||
|
||||
Future<void> _loadStats() async {
|
||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||
final cloudOnlyCount = await twonlyDB.mediaFilesDao
|
||||
.getCloudOnlyMemoriesCount();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_memoriesUsage = memoriesUsage;
|
||||
_cloudOnlyCount = cloudOnlyCount;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disableBackup() async {
|
||||
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);
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showSnackbar(context, e.toString());
|
||||
}
|
||||
}
|
||||
final disabled = await promptAndDisableMemoriesBackup(context);
|
||||
if (disabled && mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ class DataAndStorageView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||
late Future<Map<MediaType, int>> _storageStatsFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_storageStatsFuture = twonlyDB.mediaFilesDao.getStorageStats();
|
||||
}
|
||||
|
||||
Future<void> showAutoDownloadOptions(
|
||||
BuildContext context,
|
||||
ConnectivityResult connectionMode,
|
||||
|
|
@ -93,7 +101,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
return ListView(
|
||||
children: [
|
||||
FutureBuilder<Map<MediaType, int>>(
|
||||
future: twonlyDB.mediaFilesDao.getStorageStats(),
|
||||
future: _storageStatsFuture,
|
||||
builder: (context, snapshot) {
|
||||
final stats = snapshot.data ?? {};
|
||||
final totalBytes = stats.values.fold<int>(0, (a, b) => a + b);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.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: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:intl/intl.dart';
|
||||
import 'package:twonly/locator.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:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,9 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> updateUserAvatar(String json, String svg) async {
|
||||
Future<void> updateUserAvatar(String svg) async {
|
||||
await UserService.update(
|
||||
(u) => u
|
||||
..avatarJson = json
|
||||
..avatarSvg = svg
|
||||
..avatarCounter = u.avatarCounter + 1,
|
||||
);
|
||||
|
|
@ -104,9 +103,8 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
|
||||
Future<void> storeAvatarAndExit() async {
|
||||
await _avatarMakerController.saveAvatarSVG();
|
||||
final json = _avatarMakerController.getJsonOptionsSync();
|
||||
final svg = _avatarMakerController.getAvatarSVGSync();
|
||||
await updateUserAvatar(json, svg);
|
||||
await updateUserAvatar(svg);
|
||||
if (mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
|
|
@ -118,8 +116,8 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
if (_avatarMakerController.getJsonOptionsSync() !=
|
||||
userService.currentUser.avatarJson) {
|
||||
if (_avatarMakerController.getAvatarSVGSync() !=
|
||||
userService.currentUser.avatarSvg) {
|
||||
// there where changes
|
||||
final shouldPop = await _showBackDialog() ?? false;
|
||||
if (context.mounted && shouldPop) {
|
||||
|
|
|
|||
234
pubspec.lock
234
pubspec.lock
|
|
@ -18,7 +18,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.3.68"
|
||||
adaptive_number:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/adaptive_number"
|
||||
relative: true
|
||||
|
|
@ -91,19 +91,17 @@ packages:
|
|||
audio_waveforms:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_waveforms
|
||||
sha256: "03b3430ecf430a2e90185518a228c02be3d26653c62dd931e50d671213a6dbc8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "dependencies/audio_waveforms"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.0.2"
|
||||
avatar_maker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: avatar_maker
|
||||
sha256: ca182e33343846427da68fc226325f630063da2de2f0bdb49e683f0c843b80c2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
path: "dependencies/avatar_maker"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.5.0"
|
||||
background_downloader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -115,10 +113,9 @@ packages:
|
|||
blurhash_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: blurhash_dart
|
||||
sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "dependencies/blurhash_dart"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.2.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
|
|
@ -184,30 +181,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -217,14 +190,12 @@ packages:
|
|||
source: hosted
|
||||
version: "0.12.0+1"
|
||||
camera_android_camerax:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/camera/camera_android_camerax"
|
||||
ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||
resolved-ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||
url: "https://github.com/otsmr/flutter-packages.git"
|
||||
source: git
|
||||
version: "0.7.1+2"
|
||||
path: "dependencies/camera_android_camerax"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.7.4+6"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -237,10 +208,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: camera_platform_interface
|
||||
sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
|
||||
sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.1"
|
||||
camera_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -410,7 +381,7 @@ packages:
|
|||
source: hosted
|
||||
version: "8.1.0"
|
||||
dots_indicator:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/dots_indicator"
|
||||
relative: true
|
||||
|
|
@ -441,7 +412,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.3.0"
|
||||
ed25519_edwards:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/ed25519_edwards"
|
||||
relative: true
|
||||
|
|
@ -628,14 +599,6 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -986,7 +949,7 @@ packages:
|
|||
source: path
|
||||
version: "2.3.0"
|
||||
hashlib_codecs:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/hashlib_codecs"
|
||||
relative: true
|
||||
|
|
@ -1035,11 +998,10 @@ packages:
|
|||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
path: "dependencies/image"
|
||||
relative: true
|
||||
source: path
|
||||
version: "4.9.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1145,10 +1107,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
version: "0.20.3"
|
||||
introduction_screen:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1294,10 +1256,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
version: "0.12.20"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1306,14 +1268,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1361,16 +1331,8 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/optional"
|
||||
relative: true
|
||||
|
|
@ -1552,7 +1514,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/pointycastle"
|
||||
relative: true
|
||||
|
|
@ -1577,11 +1539,10 @@ packages:
|
|||
pro_video_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pro_video_editor
|
||||
sha256: cfed1424b3ca3d5981cc81efdd20b844c995c0ad2818e185eb5bc06a8674f728
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.14.2"
|
||||
path: "dependencies/pro_video_editor"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.11.3"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1623,7 +1584,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.5.0"
|
||||
qr:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/qr"
|
||||
relative: true
|
||||
|
|
@ -1658,14 +1619,6 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
screen_protector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1815,52 +1768,12 @@ packages:
|
|||
source: hosted
|
||||
version: "1.10.2"
|
||||
sprintf:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/sprintf"
|
||||
relative: true
|
||||
source: path
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1933,14 +1846,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1953,10 +1858,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.12"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2073,10 +1978,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.4.2"
|
||||
very_good_analysis:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -2088,35 +1993,32 @@ packages:
|
|||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.1"
|
||||
path: "dependencies/video_player"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.14.0"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.5"
|
||||
path: "dependencies/video_player_android"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.12.0"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
path: "dependencies/video_player_avfoundation"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.11.0"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||
sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.0"
|
||||
version: "6.9.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2222,7 +2124,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.9.1+1"
|
||||
x25519:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/x25519"
|
||||
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'
|
||||
|
||||
version: 0.5.0+168
|
||||
version: 0.5.2+172
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
|
@ -37,7 +37,6 @@ dependencies:
|
|||
path_provider: ^2.1.5
|
||||
url_launcher: ^6.3.2
|
||||
vector_graphics: ^1.1.19
|
||||
video_player: ^2.10.1
|
||||
in_app_purchase: ^3.3.0
|
||||
go_router: ^17.1.0
|
||||
|
||||
|
|
@ -72,34 +71,13 @@ dependencies:
|
|||
# Overwritten by self-controlled repository
|
||||
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.)
|
||||
app_links: ^7.0.0 # 1.6 mio
|
||||
image: ^4.3.0 # 3.3 mio
|
||||
flutter_secure_storage: ^10.3.1 # 1.85 mio
|
||||
permission_handler: ^12.0.0+1 # 2 mio
|
||||
|
||||
# Not yet checked
|
||||
audio_waveforms: ^2.0.2
|
||||
avatar_maker: ^0.4.0
|
||||
background_downloader: ^9.4.0
|
||||
cached_network_image: ^3.4.1
|
||||
cryptography_flutter_plus: ^3.0.0
|
||||
cryptography_plus: ^3.0.0
|
||||
flutter_android_volume_keydown: ^1.0.1
|
||||
|
|
@ -109,60 +87,47 @@ dependencies:
|
|||
photo_manager: ^3.9.0
|
||||
google_mlkit_barcode_scanning: ^0.14.1
|
||||
google_mlkit_face_detection: ^0.13.1
|
||||
pro_video_editor: ^1.6.1
|
||||
rust_lib_twonly:
|
||||
path: rust_builder
|
||||
flutter_rust_bridge: 2.12.0
|
||||
|
||||
## --- Start Managed Dependencies ---
|
||||
audio_waveforms: ^2.0.2
|
||||
avatar_maker: ^1.5.0
|
||||
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:
|
||||
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:
|
||||
# Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238)
|
||||
# Using override until this gets merged.
|
||||
|
|
@ -173,12 +138,74 @@ dependency_overrides:
|
|||
git:
|
||||
url: https://github.com/yenchieh/flutter_android_volume_keydown.git
|
||||
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:
|
||||
path: ./dependencies/exif
|
||||
sprintf:
|
||||
path: ./dependencies/sprintf
|
||||
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:
|
||||
build_runner: ^2.4.15
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
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/parser/base.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ class UserEnvironment {
|
|||
appVersion: 100,
|
||||
);
|
||||
|
||||
// ignore: cascade_invocations
|
||||
us.isUserCreated = true;
|
||||
|
||||
final identityKeyPair = generateIdentityKeyPair();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('BetterText parses URLs correctly', (WidgetTester tester) async {
|
||||
testWidgets('BetterText parses URLs correctly', (tester) async {
|
||||
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) !';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue