diff --git a/.gitignore b/.gitignore index f6e6d121..10e2410d 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ devtools_options.yaml rust/target rust_dependencies/target fastlane/repo/status/running.json +.cache/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2beeb76d..62f602ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 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 +58,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 +68,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 +102,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 +142,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 +172,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 +182,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 +208,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 +229,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 \ No newline at end of file +- 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 diff --git a/analysis_options.yaml b/analysis_options.yaml index bf536544..4aaf5356 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,6 +26,13 @@ analyzer: - "**.arb" - "test/drift/**" - "**.g.dart" + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** linter: rules: diff --git a/android/app/build.gradle b/android/app/build.gradle index 8ff9cb7b..e200ccee 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2f7732ad..8f648018 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ { } }); + 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, ); } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index f70abe50..3d294a52 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -275,7 +275,7 @@ class GroupsDao extends DatabaseAccessor 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 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) & diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index f79a2281..d15b85e0 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -72,7 +72,7 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { await (delete(receivedReceipts)..where( (t) => (t.createdAt.isSmallerThanValue( clock.now().subtract( - const Duration(days: 25), + const Duration(days: 45), ), )), )) diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index c364b9e6..32cb4ea2 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -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)(); diff --git a/lib/src/database/tables/receipts.table.dart b/lib/src/database/tables/receipts.table.dart index 691cc322..982bd86c 100644 --- a/lib/src/database/tables/receipts.table.dart +++ b/lib/src/database/tables/receipts.table.dart @@ -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()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 3d84320a..fca40dc8 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -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([ diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 69d947a8..1f0b67fc 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -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 diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 0ef9b545..3a8fb0db 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -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...'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index a51297d4..90201697 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -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...'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index ce51a6d0..50cc5cb6 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5 +Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486 diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 875ab4f8..82f15b09 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -23,7 +23,6 @@ class UserData { String username; String displayName; String? avatarSvg; - String? avatarJson; @JsonKey(defaultValue: 0) int appVersion = 0; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index b1449373..6cd7e9e8 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -16,7 +16,6 @@ UserData _$UserDataFromJson(Map 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 _$UserDataToJson(UserData instance) => { 'username': instance.username, 'displayName': instance.displayName, 'avatarSvg': instance.avatarSvg, - 'avatarJson': instance.avatarJson, 'appVersion': instance.appVersion, 'avatarCounter': instance.avatarCounter, 'isDeveloper': instance.isDeveloper, diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index e6b37675..e8ba7f39 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -227,11 +227,19 @@ Future 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 _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.', diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index c46a4c36..9c2aa47d 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -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, ); diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart index 77498eb5..85e7c088 100644 --- a/lib/src/services/api/server_messages.api.dart +++ b/lib/src/services/api/server_messages.api.dart @@ -141,6 +141,26 @@ Future _handleClient2ClientMessage( } if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { + Log.info( + '[$receiptId] Message is a duplicate. 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 +289,13 @@ Future _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; diff --git a/lib/src/services/group.service.dart b/lib/src/services/group.service.dart index 6e933fe3..1613b525 100644 --- a/lib/src/services/group.service.dart +++ b/lib/src/services/group.service.dart @@ -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; diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index 054397ec..71b7bb93 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -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 _pubKeysDoNotMatch(BuildContext context, String username) async { ); } -Future 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> 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 handleIntentSharedFile( BuildContext context, List 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 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: } diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index 0ac3d199..46fafde8 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -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. diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index 2cd65a36..4810048f 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -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), diff --git a/lib/src/utils/avatars.dart b/lib/src/utils/avatars.dart index 8bfe349d..ec65ff6f 100644 --- a/lib/src/utils/avatars.dart +++ b/lib/src/utils/avatars.dart @@ -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 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; + }); } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 58387db0..65299bd0 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -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 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); diff --git a/lib/src/visual/components/cached_network_image.dart b/lib/src/visual/components/cached_network_image.dart new file mode 100644 index 00000000..2bfef9fb --- /dev/null +++ b/lib/src/visual/components/cached_network_image.dart @@ -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 createState() => _CachedNetworkImageState(); +} + +class _CachedNetworkImageState extends State { + 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 _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 _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); + }, + ); + } +} diff --git a/lib/src/visual/helpers/video_player_file.helper.dart b/lib/src/visual/helpers/video_player_file.helper.dart index 93ffdbf0..ce0f8660 100644 --- a/lib/src/visual/helpers/video_player_file.helper.dart +++ b/lib/src/visual/helpers/video_player_file.helper.dart @@ -24,9 +24,7 @@ class _VideoPlayerFileHelperState extends State { super.initState(); _controller = VideoPlayerController.file( widget.videoPath, - videoPlayerOptions: VideoPlayerOptions( - mixWithOthers: true, - ), + videoPlayerOptions: VideoPlayerOptions(), ); unawaited( diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index 2934f9e9..28b97b10 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -780,10 +780,18 @@ class _CameraPreviewViewState extends State { 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 && diff --git a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart index c57a780d..e4b4916d 100644 --- a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart @@ -91,10 +91,10 @@ class SaveToGalleryButtonState extends State { 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( diff --git a/lib/src/visual/views/camera/share_image_editor.view.dart b/lib/src/visual/views/camera/share_image_editor.view.dart index f5ee5e06..91d85187 100644 --- a/lib/src/visual/views/camera/share_image_editor.view.dart +++ b/lib/src/visual/views/camera/share_image_editor.view.dart @@ -116,9 +116,7 @@ class _ShareImageEditorView extends State { }); videoController = VideoPlayerController.file( mediaService.originalPath, - videoPlayerOptions: VideoPlayerOptions( - mixWithOthers: true, - ), + videoPlayerOptions: VideoPlayerOptions(), ); videoController?.setLooping(true); videoController @@ -127,7 +125,7 @@ class _ShareImageEditorView extends State { 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); } } diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart index 8c6bfdcd..99c1ecb8 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart @@ -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 { diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart index bf30b80e..973afb02 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart @@ -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 { diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.dart index 40041bdb..a02ebce2 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.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/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart'; diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.dart index 8bdc763c..4d051dfa 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.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'; diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.dart index 4c253125..b58f9a69 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.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 { diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index d4b953ed..2c8ef886 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -84,6 +84,10 @@ class _ChatMessagesViewState extends State contactSub?.cancel(); groupActionsSub?.cancel(); _nextTypingIndicator?.cancel(); + try { + textFieldFocus?.dispose(); + // ignore: empty_catches + } catch (e) {} WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart index d076c4c5..12a2f8bd 100644 --- a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart @@ -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 { ) 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 { : 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, ], ), diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart index 694f3581..35d46979 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart @@ -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, diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart index 3fdf953b..6ca2ff66 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -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>( + 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) { diff --git a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart index fbd259c2..085a779e 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart @@ -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 { diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 5cc36dda..e52022e9 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -426,9 +426,7 @@ class _MediaViewerViewState extends State { Future _setupVideoPlayer(MediaFileService mediaLocal) async { final controller = VideoPlayerController.file( mediaLocal.tempPath, - videoPlayerOptions: VideoPlayerOptions( - mixWithOthers: mediaLocal.mediaFile.displayLimitInMilliseconds == null, - ), + videoPlayerOptions: VideoPlayerOptions(), ); await controller.setLooping( diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index 7c80aead..b86a3c55 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -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 with WidgetsBindingObserver { StreamSubscription? _onMessageOpenedAppSub; StreamSubscription? _homeViewPageIndexSub; StreamSubscription? _selectNotificationSub; + StreamSubscription<(String, MediaType)>? _sharedMediaSub; static Uri? pendingSharedLink; + static (String, MediaType)? pendingSharedMedia; static final streamHomeViewPageIndex = StreamController.broadcast(); static final streamSharedLink = StreamController.broadcast(); + static final streamSharedMedia = + StreamController<(String, MediaType)>.broadcast(); @override void initState() { @@ -59,6 +66,8 @@ class HomeViewState extends State 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 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 with WidgetsBindingObserver { _mainCameraController.setState = null; _mainCameraController.closeCamera(); _sharedLinkSub?.cancel(); + _sharedMediaSub?.cancel(); super.dispose(); } diff --git a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart index 47db82ea..65534d27 100644 --- a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart +++ b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart @@ -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().plan == SubscriptionPlan.Free; + return StreamBuilder( 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, + ), ), ), ], diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 4664cb63..4c16b273 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -306,11 +306,11 @@ class MemoriesViewState extends State 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 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 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]; diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index b1a80246..ed4c77f9 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -207,6 +207,19 @@ class _SynchronizedImageViewerScreenState Future _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( diff --git a/lib/src/visual/views/onboarding/recover_password.view.dart b/lib/src/visual/views/onboarding/recover_password.view.dart index 205949f5..84acc7ff 100644 --- a/lib/src/visual/views/onboarding/recover_password.view.dart +++ b/lib/src/visual/views/onboarding/recover_password.view.dart @@ -27,6 +27,13 @@ class _BackupRecoveryViewState extends State { final TextEditingController usernameCtrl = TextEditingController(); final TextEditingController passwordCtrl = TextEditingController(); + @override + void dispose() { + usernameCtrl.dispose(); + passwordCtrl.dispose(); + super.dispose(); + } + Future _recoverTwonlySafe() async { setState(() { isLoading = true; diff --git a/lib/src/visual/views/onboarding/register.view.dart b/lib/src/visual/views/onboarding/register.view.dart index 9d4f638b..7903b9f0 100644 --- a/lib/src/visual/views/onboarding/register.view.dart +++ b/lib/src/visual/views/onboarding/register.view.dart @@ -115,7 +115,7 @@ class _RegisterViewState extends State { 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) { diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index 3487b821..3c2dcff4 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -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 { Widget build(BuildContext context) { final currentPlan = context.watch().plan; final isFreePlan = currentPlan == SubscriptionPlan.Free; + final hasPasswordless = + userService.currentUser.passwordLessRecovery != null; + final hasPassword = userService.currentUser.isBackupEnabled; return StreamBuilder( stream: userService.onUserUpdated, @@ -86,7 +91,33 @@ class _BackupViewState extends State { 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 { 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 { ), ), ), - 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), ], ), ), diff --git a/lib/src/visual/views/settings/backup/backup_setup.view.dart b/lib/src/visual/views/settings/backup/backup_setup.view.dart index 7a8d4277..08eef593 100644 --- a/lib/src/visual/views/settings/backup/backup_setup.view.dart +++ b/lib/src/visual/views/settings/backup/backup_setup.view.dart @@ -154,8 +154,8 @@ class _SetupBackupViewState extends State { const SizedBox(width: 8), Text( userService.currentUser.isBackupEnabled - ? context.lang.backupEnableBackup - : context.lang.backupChangePassword, + ? context.lang.backupChangePassword + : context.lang.backupEnableBackup, ), ], ), diff --git a/lib/src/visual/views/settings/backup/backup_utils.dart b/lib/src/visual/views/settings/backup/backup_utils.dart new file mode 100644 index 00000000..67c09e08 --- /dev/null +++ b/lib/src/visual/views/settings/backup/backup_utils.dart @@ -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 promptAndDisableMemoriesBackup(BuildContext context) async { + final cloudOnlyCount = + await twonlyDB.mediaFilesDao.getCloudOnlyMemoriesCount(); + + if (!context.mounted) return false; + + final confirmed = await showDialog( + 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; +} diff --git a/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart b/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart index 92402b41..fb108896 100644 --- a/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart +++ b/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart @@ -22,7 +22,9 @@ class _MissingBackupCompState extends State { 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(); } diff --git a/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart b/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart new file mode 100644 index 00000000..abf29333 --- /dev/null +++ b/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart @@ -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, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart index b0c6b761..98f4d4c4 100644 --- a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart +++ b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart @@ -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 { server.Response_MemoriesUsage? _memoriesUsage; - int _cloudOnlyCount = 0; bool _isLoading = true; @override @@ -35,110 +30,18 @@ class _MemoriesBackupDetailViewState extends State { Future _loadStats() async { final memoriesUsage = await apiService.getMemoriesUsage(); - final cloudOnlyCount = await twonlyDB.mediaFilesDao - .getCloudOnlyMemoriesCount(); if (mounted) { setState(() { _memoriesUsage = memoriesUsage; - _cloudOnlyCount = cloudOnlyCount; _isLoading = false; }); } } Future _disableBackup() async { - final confirmed = await showDialog( - 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); } } diff --git a/lib/src/visual/views/settings/data_and_storage.view.dart b/lib/src/visual/views/settings/data_and_storage.view.dart index 161adfe2..9b3a38e4 100644 --- a/lib/src/visual/views/settings/data_and_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage.view.dart @@ -17,6 +17,14 @@ class DataAndStorageView extends StatefulWidget { } class _DataAndStorageViewState extends State { + late Future> _storageStatsFuture; + + @override + void initState() { + super.initState(); + _storageStatsFuture = twonlyDB.mediaFilesDao.getStorageStats(); + } + Future showAutoDownloadOptions( BuildContext context, ConnectivityResult connectionMode, @@ -93,7 +101,7 @@ class _DataAndStorageViewState extends State { return ListView( children: [ FutureBuilder>( - future: twonlyDB.mediaFilesDao.getStorageStats(), + future: _storageStatsFuture, builder: (context, snapshot) { final stats = snapshot.data ?? {}; final totalBytes = stats.values.fold(0, (a, b) => a + b); diff --git a/lib/src/visual/views/settings/help/credits.view.dart b/lib/src/visual/views/settings/help/credits.view.dart index e015ed94..94ad3d52 100644 --- a/lib/src/visual/views/settings/help/credits.view.dart +++ b/lib/src/visual/views/settings/help/credits.view.dart @@ -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'; diff --git a/lib/src/visual/views/settings/help/news.view.dart b/lib/src/visual/views/settings/help/news.view.dart index c0ad7a66..f15e33ff 100644 --- a/lib/src/visual/views/settings/help/news.view.dart +++ b/lib/src/visual/views/settings/help/news.view.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'; diff --git a/lib/src/visual/views/settings/profile/modify_avatar.view.dart b/lib/src/visual/views/settings/profile/modify_avatar.view.dart index 7b533a78..c75a476e 100644 --- a/lib/src/visual/views/settings/profile/modify_avatar.view.dart +++ b/lib/src/visual/views/settings/profile/modify_avatar.view.dart @@ -31,10 +31,9 @@ class _ModifyAvatarViewState extends State { } } - Future updateUserAvatar(String json, String svg) async { + Future 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 { Future 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 { 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) { diff --git a/pubspec.lock b/pubspec.lock index 90bbb4de..42528d22 100644 --- a/pubspec.lock +++ b/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 diff --git a/pubspec.yaml b/pubspec.yaml index 2f295d77..1d672695 100644 --- a/pubspec.yaml +++ b/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.1+171 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 diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index c48c2b31..b0d851cb 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -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'; diff --git a/test/mocks/user_environment.dart b/test/mocks/user_environment.dart index be4e21c0..f2056b30 100644 --- a/test/mocks/user_environment.dart +++ b/test/mocks/user_environment.dart @@ -99,7 +99,6 @@ class UserEnvironment { appVersion: 100, ); - // ignore: cascade_invocations us.isUserCreated = true; final identityKeyPair = generateIdentityKeyPair(); diff --git a/test/visual/elements/better_text_element_test.dart b/test/visual/elements/better_text_element_test.dart index eca25644..3e4b6efa 100644 --- a/test/visual/elements/better_text_element_test.dart +++ b/test/visual/elements/better_text_element_test.dart @@ -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) !';