From 7007e7b0637ba56ff14e402b711b2006b8a99396 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 22 Dec 2025 15:08:11 +0100 Subject: [PATCH 01/14] fix null pointer --- lib/src/views/memories/memories.view.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 524cd5d..3d48f82 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -145,6 +145,6 @@ class MemoriesViewState extends State { // reverseTransitionDuration: Duration.zero, ), ) as bool?; - setState(() {}); + if (mounted) setState(() {}); } } diff --git a/pubspec.yaml b/pubspec.yaml index 63a5f86..c7f18c8 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.0.78+78 +version: 0.0.79+79 environment: sdk: ^3.6.0 From 82f4c9af9f9117f62710810dae0c410e9ad3c158 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 08:20:42 +0100 Subject: [PATCH 02/14] fix #353 --- .../camera_preview_controller_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 024be7b..a9e7465 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -337,10 +337,10 @@ class _CameraPreviewViewState extends State { return; } - if (Platform.isIOS) { - // android has a problem with this. Flash is turned off in the pausePreview function. + if (mc.cameraController?.value.flashMode != FlashMode.off) { await mc.cameraController?.setFlashMode(FlashMode.off); } + if (!mounted) { return; } From 027871290d9358e84f7e46e371a1f424f686c2fe Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 09:07:41 +0100 Subject: [PATCH 03/14] fix #348 --- lib/app.dart | 10 +- lib/src/database/twonly.db.g.dart | 194 +++++++++--------- lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + lib/src/model/json/userdata.dart | 3 + lib/src/model/json/userdata.g.dart | 4 +- lib/src/views/home.view.dart | 8 +- lib/src/views/settings/appearance.view.dart | 36 ++-- 11 files changed, 151 insertions(+), 122 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 67b06e5..7831a05 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; @@ -157,11 +158,13 @@ class _AppMainWidgetState extends State { bool _showOnboarding = true; bool _isLoaded = false; bool _skipBackup = false; + int _initialPage = 0; (Future?, bool) _proofOfWork = (null, false); @override void initState() { + _initialPage = widget.initialPage; initAsync(); super.initState(); } @@ -173,6 +176,9 @@ class _AppMainWidgetState extends State { if (gUser.appVersion < 62) { _showDatabaseMigration = true; } + if (!gUser.startWithCameraOpen) { + _initialPage = 0; + } } if (!_isUserCreated && !_showDatabaseMigration) { @@ -205,7 +211,7 @@ class _AppMainWidgetState extends State { if (_showDatabaseMigration) { child = const DatabaseMigrationView(); } else if (_isUserCreated) { - if (gUser.twonlySafeBackup == null && !_skipBackup) { + if (gUser.twonlySafeBackup == null && !_skipBackup && kReleaseMode) { child = TwonlyIdentityBackupView( callBack: () { _skipBackup = true; @@ -214,7 +220,7 @@ class _AppMainWidgetState extends State { ); } else { child = HomeView( - initialPage: widget.initialPage, + initialPage: _initialPage, ); } } else if (_showOnboarding) { diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 264a8c7..91f1ba9 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -7111,10 +7111,7 @@ class $GroupHistoriesTable extends GroupHistories @override late final GeneratedColumn affectedContactId = GeneratedColumn( 'affected_contact_id', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + type: DriftSqlType.int, requiredDuringInsert: false); static const VerificationMeta _oldGroupNameMeta = const VerificationMeta('oldGroupName'); @override @@ -7896,6 +7893,22 @@ final class $$ContactsTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } + + static MultiTypedResultKey<$GroupHistoriesTable, List> + _groupHistoriesRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.groupHistories, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.groupHistories.contactId)); + + $$GroupHistoriesTableProcessedTableManager get groupHistoriesRefs { + final manager = $$GroupHistoriesTableTableManager($_db, $_db.groupHistories) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupHistoriesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } } class $$ContactsTableFilterComposer @@ -8078,6 +8091,27 @@ class $$ContactsTableFilterComposer )); return f(composer); } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableFilterComposer f) f) { + final $$GroupHistoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableFilterComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$ContactsTableOrderingComposer @@ -8311,6 +8345,27 @@ class $$ContactsTableAnnotationComposer )); return f(composer); } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableAnnotationComposer a) f) { + final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableAnnotationComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$ContactsTableTableManager extends RootTableManager< @@ -8330,7 +8385,8 @@ class $$ContactsTableTableManager extends RootTableManager< bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, - bool signalContactSignedPreKeysRefs})> { + bool signalContactSignedPreKeysRefs, + bool groupHistoriesRefs})> { $$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table) : super(TableManagerState( db: db, @@ -8411,7 +8467,8 @@ class $$ContactsTableTableManager extends RootTableManager< groupMembersRefs = false, receiptsRefs = false, signalContactPreKeysRefs = false, - signalContactSignedPreKeysRefs = false}) { + signalContactSignedPreKeysRefs = false, + groupHistoriesRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ @@ -8421,7 +8478,8 @@ class $$ContactsTableTableManager extends RootTableManager< if (receiptsRefs) db.receipts, if (signalContactPreKeysRefs) db.signalContactPreKeys, if (signalContactSignedPreKeysRefs) - db.signalContactSignedPreKeys + db.signalContactSignedPreKeys, + if (groupHistoriesRefs) db.groupHistories ], addJoins: null, getPrefetchedDataCallback: (items) async { @@ -8501,6 +8559,19 @@ class $$ContactsTableTableManager extends RootTableManager< referencedItemsForCurrentItem: (item, referencedItems) => referencedItems .where((e) => e.contactId == item.userId), + typedResults: items), + if (groupHistoriesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._groupHistoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .groupHistoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), typedResults: items) ]; }, @@ -8526,7 +8597,8 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, - bool signalContactSignedPreKeysRefs})>; + bool signalContactSignedPreKeysRefs, + bool groupHistoriesRefs})>; typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ required String groupId, Value isGroupAdmin, @@ -13600,21 +13672,6 @@ final class $$GroupHistoriesTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } - - static $ContactsTable _affectedContactIdTable(_$TwonlyDB db) => - db.contacts.createAlias($_aliasNameGenerator( - db.groupHistories.affectedContactId, db.contacts.userId)); - - $$ContactsTableProcessedTableManager? get affectedContactId { - final $_column = $_itemColumn('affected_contact_id'); - if ($_column == null) return null; - final manager = $$ContactsTableTableManager($_db, $_db.contacts) - .filter((f) => f.userId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_affectedContactIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } } class $$GroupHistoriesTableFilterComposer @@ -13630,6 +13687,10 @@ class $$GroupHistoriesTableFilterComposer column: $table.groupHistoryId, builder: (column) => ColumnFilters(column)); + ColumnFilters get affectedContactId => $composableBuilder( + column: $table.affectedContactId, + builder: (column) => ColumnFilters(column)); + ColumnFilters get oldGroupName => $composableBuilder( column: $table.oldGroupName, builder: (column) => ColumnFilters(column)); @@ -13688,26 +13749,6 @@ class $$GroupHistoriesTableFilterComposer )); return composer; } - - $$ContactsTableFilterComposer get affectedContactId { - final $$ContactsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.affectedContactId, - referencedTable: $db.contacts, - getReferencedColumn: (t) => t.userId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ContactsTableFilterComposer( - $db: $db, - $table: $db.contacts, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } } class $$GroupHistoriesTableOrderingComposer @@ -13723,6 +13764,10 @@ class $$GroupHistoriesTableOrderingComposer column: $table.groupHistoryId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get affectedContactId => $composableBuilder( + column: $table.affectedContactId, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get oldGroupName => $composableBuilder( column: $table.oldGroupName, builder: (column) => ColumnOrderings(column)); @@ -13781,26 +13826,6 @@ class $$GroupHistoriesTableOrderingComposer )); return composer; } - - $$ContactsTableOrderingComposer get affectedContactId { - final $$ContactsTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.affectedContactId, - referencedTable: $db.contacts, - getReferencedColumn: (t) => t.userId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ContactsTableOrderingComposer( - $db: $db, - $table: $db.contacts, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } } class $$GroupHistoriesTableAnnotationComposer @@ -13815,6 +13840,9 @@ class $$GroupHistoriesTableAnnotationComposer GeneratedColumn get groupHistoryId => $composableBuilder( column: $table.groupHistoryId, builder: (column) => column); + GeneratedColumn get affectedContactId => $composableBuilder( + column: $table.affectedContactId, builder: (column) => column); + GeneratedColumn get oldGroupName => $composableBuilder( column: $table.oldGroupName, builder: (column) => column); @@ -13871,26 +13899,6 @@ class $$GroupHistoriesTableAnnotationComposer )); return composer; } - - $$ContactsTableAnnotationComposer get affectedContactId { - final $$ContactsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.affectedContactId, - referencedTable: $db.contacts, - getReferencedColumn: (t) => t.userId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ContactsTableAnnotationComposer( - $db: $db, - $table: $db.contacts, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } } class $$GroupHistoriesTableTableManager extends RootTableManager< @@ -13904,8 +13912,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< $$GroupHistoriesTableUpdateCompanionBuilder, (GroupHistory, $$GroupHistoriesTableReferences), GroupHistory, - PrefetchHooks Function( - {bool groupId, bool contactId, bool affectedContactId})> { + PrefetchHooks Function({bool groupId, bool contactId})> { $$GroupHistoriesTableTableManager(_$TwonlyDB db, $GroupHistoriesTable table) : super(TableManagerState( db: db, @@ -13974,8 +13981,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< $$GroupHistoriesTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: ( - {groupId = false, contactId = false, affectedContactId = false}) { + prefetchHooksCallback: ({groupId = false, contactId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -14014,17 +14020,6 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< .userId, ) as T; } - if (affectedContactId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.affectedContactId, - referencedTable: $$GroupHistoriesTableReferences - ._affectedContactIdTable(db), - referencedColumn: $$GroupHistoriesTableReferences - ._affectedContactIdTable(db) - .userId, - ) as T; - } return state; }, @@ -14047,8 +14042,7 @@ typedef $$GroupHistoriesTableProcessedTableManager = ProcessedTableManager< $$GroupHistoriesTableUpdateCompanionBuilder, (GroupHistory, $$GroupHistoriesTableReferences), GroupHistory, - PrefetchHooks Function( - {bool groupId, bool contactId, bool affectedContactId})>; + PrefetchHooks Function({bool groupId, bool contactId})>; class $TwonlyDBManager { final _$TwonlyDB _db; diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 6b1b244..4b89568 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -456,5 +456,6 @@ "linkFromUsernameLong": "Wenn du den Link von der Person direkt erhalten hast, kannst du den Kontakt als verifiziert markieren, da der öffentliche Schlüssel im Link mit dem bereits für diesen Benutzer gespeicherten öffentlichen Schlüssel übereinstimmt.", "gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.", "couldNotVerifyUsername": "{username} konnte nicht verifiziert werden", - "linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!" + "linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!", + "startWithCameraOpen": "Mit geöffneter Kamera starten" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 8610d62..1e17c59 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -486,5 +486,6 @@ "linkFromUsernameLong": "If you received the link from your friend, you can mark the user as verified, as the public key in the link matches the public key already stored for that user?", "gotLinkFromFriend": "Yes, I got the link from my friend!", "couldNotVerifyUsername": "Could not verify {username}", - "linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!" + "linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!", + "startWithCameraOpen": "Start with camera open" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index dbc4bc6..a061df1 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2839,6 +2839,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!'** String get linkPubkeyDoesNotMatch; + + /// No description provided for @startWithCameraOpen. + /// + /// In en, this message translates to: + /// **'Start with camera open'** + String get startWithCameraOpen; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 2907470..0773d7a 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1570,4 +1570,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get linkPubkeyDoesNotMatch => 'Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!'; + + @override + String get startWithCameraOpen => 'Mit geöffneter Kamera starten'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index b47d13d..90869f3 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1560,4 +1560,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get linkPubkeyDoesNotMatch => 'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!'; + + @override + String get startWithCameraOpen => 'Start with camera open'; } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index cede1c9..e4547e5 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -59,6 +59,9 @@ class UserData { @JsonKey(defaultValue: true) bool showFeedbackShortcut = true; + @JsonKey(defaultValue: true) + bool startWithCameraOpen = true; + List? preSelectedEmojies; Map>? autoDownloadOptions; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 4816031..7e60f22 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -32,6 +32,7 @@ UserData _$UserDataFromJson(Map json) => UserData( ..requestedAudioPermission = json['requestedAudioPermission'] as bool? ?? false ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true + ..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true ..preSelectedEmojies = (json['preSelectedEmojies'] as List?) ?.map((e) => e as String) .toList() @@ -61,7 +62,7 @@ UserData _$UserDataFromJson(Map json) => UserData( ..lastChangeLogHash = (json['lastChangeLogHash'] as List?) ?.map((e) => (e as num).toInt()) .toList() - ..hideChangeLog = json['hideChangeLog'] as bool? ?? false + ..hideChangeLog = json['hideChangeLog'] as bool? ?? true ..updateFCMToken = json['updateFCMToken'] as bool? ?? true ..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null ? null @@ -93,6 +94,7 @@ Map _$UserDataToJson(UserData instance) => { 'defaultShowTime': instance.defaultShowTime, 'requestedAudioPermission': instance.requestedAudioPermission, 'showFeedbackShortcut': instance.showFeedbackShortcut, + 'startWithCameraOpen': instance.startWithCameraOpen, 'preSelectedEmojies': instance.preSelectedEmojies, 'autoDownloadOptions': instance.autoDownloadOptions, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 282dfae..bb065a4 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -230,10 +230,10 @@ class HomeViewState extends State { final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); - if (notificationAppLaunchDetails != null) { - if (notificationAppLaunchDetails.didNotificationLaunchApp) { - globalUpdateOfHomeViewPageIndex(0); - } + if (widget.initialPage == 0 || + (notificationAppLaunchDetails != null && + notificationAppLaunchDetails.didNotificationLaunchApp)) { + globalUpdateOfHomeViewPageIndex(0); } final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile(); diff --git a/lib/src/views/settings/appearance.view.dart b/lib/src/views/settings/appearance.view.dart index 34e0688..8f8c338 100644 --- a/lib/src/views/settings/appearance.view.dart +++ b/lib/src/views/settings/appearance.view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -15,20 +16,9 @@ class AppearanceView extends StatefulWidget { } class _AppearanceViewState extends State { - bool showFeedbackShortcut = false; - @override void initState() { super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - final user = await getUser(); - if (user == null) return; - setState(() { - showFeedbackShortcut = user.showFeedbackShortcut; - }); } Future _showSelectThemeMode(BuildContext context) async { @@ -87,7 +77,19 @@ class _AppearanceViewState extends State { u.showFeedbackShortcut = !u.showFeedbackShortcut; return u; }); - await initAsync(); + setState(() { + // gUser + }); + } + + Future toggleStartWithCameraOpen() async { + await updateUserdata((u) { + u.startWithCameraOpen = !u.startWithCameraOpen; + return u; + }); + setState(() { + // gUser + }); } @override @@ -113,10 +115,18 @@ class _AppearanceViewState extends State { title: Text(context.lang.contactUsShortcut), onTap: toggleShowFeedbackIcon, trailing: Switch( - value: !showFeedbackShortcut, + value: !gUser.showFeedbackShortcut, onChanged: (a) => toggleShowFeedbackIcon(), ), ), + ListTile( + title: Text(context.lang.startWithCameraOpen), + onTap: toggleStartWithCameraOpen, + trailing: Switch( + value: gUser.startWithCameraOpen, + onChanged: (a) => toggleStartWithCameraOpen(), + ), + ), ], ), ); From abd689f1fa5b167f3c67d7836ebd6c43d4198310 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 10:20:46 +0100 Subject: [PATCH 04/14] fix #352 --- lib/src/views/chats/chat_messages.view.dart | 50 +++++-------- lib/src/views/components/blink.component.dart | 75 +++++++++++++++++++ 2 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 lib/src/views/components/blink.component.dart diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 4ba73e2..273aca8 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -18,6 +18,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_dat import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/blink.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; @@ -372,36 +373,25 @@ class _ChatMessagesViewState extends State { ); } else { final chatMessage = messages[i].message!; - return Transform.translate( - offset: Offset( - (focusedScrollItem == i) - ? (chatMessage.senderId == null) - ? -8 - : 8 - : 0, - 0, - ), - child: Transform.scale( - scale: (focusedScrollItem == i) ? 1.05 : 1, - child: ChatListEntry( - key: Key(chatMessage.messageId), - message: messages[i].message!, - nextMessage: - (i > 0) ? messages[i - 1].message : null, - prevMessage: ((i + 1) < messages.length) - ? messages[i + 1].message - : null, - group: group, - galleryItems: galleryItems, - userIdToContact: userIdToContact, - scrollToMessage: scrollToMessage, - onResponseTriggered: () { - setState(() { - quotesMessage = chatMessage; - }); - textFieldFocus.requestFocus(); - }, - ), + return BlinkWidget( + enabled: focusedScrollItem == i, + child: ChatListEntry( + key: Key(chatMessage.messageId), + message: messages[i].message!, + nextMessage: (i > 0) ? messages[i - 1].message : null, + prevMessage: ((i + 1) < messages.length) + ? messages[i + 1].message + : null, + group: group, + galleryItems: galleryItems, + userIdToContact: userIdToContact, + scrollToMessage: scrollToMessage, + onResponseTriggered: () { + setState(() { + quotesMessage = chatMessage; + }); + textFieldFocus.requestFocus(); + }, ), ); } diff --git a/lib/src/views/components/blink.component.dart b/lib/src/views/components/blink.component.dart new file mode 100644 index 0000000..3a9c094 --- /dev/null +++ b/lib/src/views/components/blink.component.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class BlinkWidget extends StatefulWidget { + const BlinkWidget({ + required this.child, + required this.enabled, + super.key, + this.blinkDuration = const Duration(milliseconds: 2500), + this.interval = const Duration(milliseconds: 250), + this.visibleOpacity = 1.0, + this.hiddenOpacity = 0.4, + }); + final bool enabled; + final Widget child; + final Duration blinkDuration; + final Duration interval; + final double visibleOpacity; + final double hiddenOpacity; + + @override + State createState() => _BlinkWidgetState(); +} + +class _BlinkWidgetState extends State + with SingleTickerProviderStateMixin { + late Ticker _ticker; + bool _visible = true; + + @override + void initState() { + super.initState(); + _ticker = createTicker(_onTick); + } + + @override + void didUpdateWidget(covariant BlinkWidget oldWidget) { + if (oldWidget.enabled != widget.enabled) { + if (widget.enabled) { + _ticker + ..stop() + ..start(); + } + } + super.didUpdateWidget(oldWidget); + } + + void _onTick(Duration elapsed) { + var visible = true; + if (elapsed.inMilliseconds < widget.blinkDuration.inMilliseconds) { + visible = elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) < + widget.interval.inMilliseconds; + } else { + _ticker.stop(); + } + setState(() => _visible = visible); + } + + @override + void dispose() { + _ticker.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + opacity: _visible ? widget.visibleOpacity : widget.hiddenOpacity, + duration: Duration( + milliseconds: widget.blinkDuration.inMilliseconds ~/ 3, + ), + child: widget.child, + ); + } +} From 27483bccd6b69863685c5b0915b6973b36147eaa Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 12:12:56 +0100 Subject: [PATCH 05/14] workaround for #349 --- lib/src/services/api.service.dart | 2 +- .../camera_preview_controller_view.dart | 1 + .../views/camera/image_editor/data/layer.dart | 10 +++++++ .../image_editor/layers/emoji_layer.dart | 5 ++++ .../camera/image_editor/layers_viewer.dart | 7 +++-- .../image_editor/modules/all_emojis.dart | 1 + .../views/camera/share_image_editor_view.dart | 29 +++++++++++++------ 7 files changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 36ad7bc..223ea00 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -50,7 +50,7 @@ final lockRetransStore = Mutex(); /// errors or network changes. class ApiService { ApiService(); - final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030'; + final String apiHost = kReleaseMode ? 'api.twonly.eu' : '192.168.178.88:3030'; // final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; final String apiSecure = kReleaseMode ? 's' : ''; diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index a9e7465..25f299b 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -397,6 +397,7 @@ class _CameraPreviewViewState extends State { sharedFromGallery: sharedFromGallery, sendToGroup: widget.sendToGroup, mediaFileService: mediaFileService, + mainCameraController: mc, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; diff --git a/lib/src/views/camera/image_editor/data/layer.dart b/lib/src/views/camera/image_editor/data/layer.dart index 7f6bdf8..fb1fdd9 100755 --- a/lib/src/views/camera/image_editor/data/layer.dart +++ b/lib/src/views/camera/image_editor/data/layer.dart @@ -7,6 +7,7 @@ import 'package:twonly/src/views/camera/image_editor/data/image_item.dart'; /// Layer class with some common properties class Layer { Layer({ + required this.key, this.offset = Offset.zero, this.opacity = 1, this.isEditing = false, @@ -16,6 +17,7 @@ class Layer { this.rotation = 0, this.scale = 1, }); + Key key; Offset offset; double rotation; double scale; @@ -29,18 +31,24 @@ class Layer { /// Attributes used by [BackgroundLayer] class BackgroundLayerData extends Layer { BackgroundLayerData({ + required super.key, required this.image, }); ImageItem image; } class FilterLayerData extends Layer { + FilterLayerData({ + required super.key, + this.page = 1, + }); int page = 1; } /// Attributes used by [EmojiLayer] class EmojiLayerData extends Layer { EmojiLayerData({ + required super.key, this.text = '', this.size = 64, super.offset, @@ -56,6 +64,7 @@ class EmojiLayerData extends Layer { /// Attributes used by [TextLayer] class TextLayerData extends Layer { TextLayerData({ + required super.key, required this.textLayersBefore, this.text = '', super.offset, @@ -72,6 +81,7 @@ class TextLayerData extends Layer { class DrawLayerData extends Layer { // String text; DrawLayerData({ + required super.key, super.offset, super.opacity, super.rotation, diff --git a/lib/src/views/camera/image_editor/layers/emoji_layer.dart b/lib/src/views/camera/image_editor/layers/emoji_layer.dart index 0e5b7d7..32ddcc3 100755 --- a/lib/src/views/camera/image_editor/layers/emoji_layer.dart +++ b/lib/src/views/camera/image_editor/layers/emoji_layer.dart @@ -119,6 +119,11 @@ class _EmojiLayerState extends State { setState(() { twoPointerWhereDown = details.pointerCount >= 2; widget.layerData.size = initialScale * details.scale; + if (widget.layerData.size > 96) { + // https://github.com/twonlyapp/twonly-app/issues/349 + widget.layerData.size = 96; + } + // print(widget.layerData.size); widget.layerData.rotation = initialRotation + details.rotation; diff --git a/lib/src/views/camera/image_editor/layers_viewer.dart b/lib/src/views/camera/image_editor/layers_viewer.dart index 0fcf148..11312d1 100644 --- a/lib/src/views/camera/image_editor/layers_viewer.dart +++ b/lib/src/views/camera/image_editor/layers_viewer.dart @@ -23,12 +23,14 @@ class LayersViewer extends StatelessWidget { children: [ ...layers.whereType().map((layerItem) { return BackgroundLayer( + key: layerItem.key, layerData: layerItem, onUpdate: onUpdate, ); }), ...layers.whereType().map((layerItem) { return FilterLayer( + key: layerItem.key, layerData: layerItem, ); }), @@ -40,12 +42,13 @@ class LayersViewer extends StatelessWidget { .map((layerItem) { if (layerItem is EmojiLayerData) { return EmojiLayer( - key: GlobalKey(), + key: layerItem.key, layerData: layerItem, onUpdate: onUpdate, ); } else if (layerItem is DrawLayerData) { return DrawLayer( + key: layerItem.key, layerData: layerItem, onUpdate: onUpdate, ); @@ -54,7 +57,7 @@ class LayersViewer extends StatelessWidget { }), ...layers.whereType().map((layerItem) { return TextLayer( - // key: GlobalKey(), + key: layerItem.key, layerData: layerItem, onUpdate: onUpdate, ); diff --git a/lib/src/views/camera/image_editor/modules/all_emojis.dart b/lib/src/views/camera/image_editor/modules/all_emojis.dart index c808bd0..a19d30d 100755 --- a/lib/src/views/camera/image_editor/modules/all_emojis.dart +++ b/lib/src/views/camera/image_editor/modules/all_emojis.dart @@ -44,6 +44,7 @@ class EmojiPickerBottom extends StatelessWidget { Navigator.pop( context, EmojiLayerData( + key: GlobalKey(), text: emoji.emoji, ), ); diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 1e69bea..8d19205 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -16,6 +16,7 @@ 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/utils/storage.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/image_editor/data/image_item.dart'; @@ -32,17 +33,18 @@ List undoLayers = []; List removedLayers = []; class ShareImageEditorView extends StatefulWidget { - const ShareImageEditorView({ - required this.sharedFromGallery, - required this.mediaFileService, - super.key, - this.imageBytesFuture, - this.sendToGroup, - }); + const ShareImageEditorView( + {required this.sharedFromGallery, + required this.mediaFileService, + super.key, + this.imageBytesFuture, + this.sendToGroup, + this.mainCameraController}); final Future? imageBytesFuture; final Group? sendToGroup; final bool sharedFromGallery; final MediaFileService mediaFileService; + final MainCameraController? mainCameraController; @override State createState() => _ShareImageEditorView(); } @@ -69,7 +71,7 @@ class _ShareImageEditorView extends State { super.initState(); if (media.type != MediaType.gif) { - layers.add(FilterLayerData()); + layers.add(FilterLayerData(key: GlobalKey())); } if (widget.sendToGroup != null) { @@ -147,6 +149,7 @@ class _ShareImageEditorView extends State { removedLayers.clear(); layers.add( TextLayerData( + key: GlobalKey(), textLayersBefore: layers.whereType().length, ), ); @@ -161,7 +164,7 @@ class _ShareImageEditorView extends State { onPressed: () async { undoLayers.clear(); removedLayers.clear(); - layers.add(DrawLayerData()); + layers.add(DrawLayerData(key: GlobalKey())); setState(() {}); }, ), @@ -458,9 +461,16 @@ class _ShareImageEditorView extends State { if (!context.mounted) return; + Future.delayed(const Duration(milliseconds: 500), () async { + if (context.mounted) { + await widget.mainCameraController?.closeCamera(); + } + }); + layers.insert( 0, BackgroundLayerData( + key: GlobalKey(), image: currentImage, ), ); @@ -526,6 +536,7 @@ class _ShareImageEditorView extends State { removedLayers.clear(); layers.add( TextLayerData( + key: GlobalKey(), offset: Offset(0, tabDownPosition), textLayersBefore: layers.whereType().length, ), From 910f5f79fac3f3de7bbcbe2acff32c2760f48152 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 21:10:32 +0100 Subject: [PATCH 06/14] make images visible before sending #356 and remove dependencies #333 --- dependencies | 2 +- lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 4 + .../generated/app_localizations_en.dart | 4 + lib/src/model/json/userdata.dart | 3 + lib/src/model/json/userdata.g.dart | 4 + .../api/mediafiles/upload.service.dart | 8 ++ lib/src/utils/screenshot.dart | 104 ++++++++++++++++++ .../camera_preview.dart | 2 +- .../camera_preview_controller_view.dart | 15 ++- .../main_camera_controller.dart | 2 +- .../save_to_gallery.dart | 3 +- .../image_editor/layers/draw_layer.dart | 2 +- .../views/camera/share_image_editor_view.dart | 67 ++++++----- lib/src/views/camera/share_image_view.dart | 34 +++++- .../message_input.dart | 1 + .../message_send_state_icon.dart | 3 +- lib/src/views/settings/appearance.view.dart | 18 +++ pubspec.lock | 15 +-- pubspec.yaml | 5 +- 22 files changed, 244 insertions(+), 64 deletions(-) create mode 100644 lib/src/utils/screenshot.dart diff --git a/dependencies b/dependencies index fb66274..83475a9 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit fb66274bf729cde6f7184ec6f7f9ea89f12450fd +Subproject commit 83475a912851acb6a718ea32a6f0f754d64a50d8 diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 4b89568..8cb4d76 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -457,5 +457,6 @@ "gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.", "couldNotVerifyUsername": "{username} konnte nicht verifiziert werden", "linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!", - "startWithCameraOpen": "Mit geöffneter Kamera starten" + "startWithCameraOpen": "Mit geöffneter Kamera starten", + "showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 1e17c59..ebc9f7f 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -487,5 +487,6 @@ "gotLinkFromFriend": "Yes, I got the link from my friend!", "couldNotVerifyUsername": "Could not verify {username}", "linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!", - "startWithCameraOpen": "Start with camera open" + "startWithCameraOpen": "Start with camera open", + "showImagePreviewWhenSending": "Display image preview when selecting recipients" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index a061df1..8d497c3 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2845,6 +2845,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Start with camera open'** String get startWithCameraOpen; + + /// No description provided for @showImagePreviewWhenSending. + /// + /// In en, this message translates to: + /// **'Display image preview when selecting recipients'** + String get showImagePreviewWhenSending; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 0773d7a..49ab691 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1573,4 +1573,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get startWithCameraOpen => 'Mit geöffneter Kamera starten'; + + @override + String get showImagePreviewWhenSending => + 'Bildvorschau bei der Auswahl von Empfängern anzeigen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 90869f3..064d769 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1563,4 +1563,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get startWithCameraOpen => 'Start with camera open'; + + @override + String get showImagePreviewWhenSending => + 'Display image preview when selecting recipients'; } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index e4547e5..ad0852d 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -59,6 +59,9 @@ class UserData { @JsonKey(defaultValue: true) bool showFeedbackShortcut = true; + @JsonKey(defaultValue: true) + bool showShowImagePreviewWhenSending = true; + @JsonKey(defaultValue: true) bool startWithCameraOpen = true; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 7e60f22..8e2103d 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -32,6 +32,8 @@ UserData _$UserDataFromJson(Map json) => UserData( ..requestedAudioPermission = json['requestedAudioPermission'] as bool? ?? false ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true + ..showShowImagePreviewWhenSending = + json['showShowImagePreviewWhenSending'] as bool? ?? true ..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true ..preSelectedEmojies = (json['preSelectedEmojies'] as List?) ?.map((e) => e as String) @@ -94,6 +96,8 @@ Map _$UserDataToJson(UserData instance) => { 'defaultShowTime': instance.defaultShowTime, 'requestedAudioPermission': instance.requestedAudioPermission, 'showFeedbackShortcut': instance.showFeedbackShortcut, + 'showShowImagePreviewWhenSending': + instance.showShowImagePreviewWhenSending, 'startWithCameraOpen': instance.startWithCameraOpen, 'preSelectedEmojies': instance.preSelectedEmojies, 'autoDownloadOptions': instance.autoDownloadOptions, diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 0c2e930..afbe91d 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -87,6 +87,7 @@ Future initializeMediaUpload( Future insertMediaFileInMessagesTable( MediaFileService mediaService, List groupIds, + Future? imageStoreAwait, ) async { await twonlyDB.mediaFilesDao.updateAllMediaFiles( const MediaFilesCompanion( @@ -117,6 +118,13 @@ Future insertMediaFileInMessagesTable( } } + if (imageStoreAwait != null) { + if (await imageStoreAwait == null) { + Log.error('image store as original did return false...'); + return; + } + } + unawaited(startBackgroundMediaUpload(mediaService)); } diff --git a/lib/src/utils/screenshot.dart b/lib/src/utils/screenshot.dart new file mode 100644 index 0000000..1bf4517 --- /dev/null +++ b/lib/src/utils/screenshot.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui' as io; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:twonly/src/utils/log.dart'; + +class ScreenshotImage { + ScreenshotImage({ + this.image, + this.imageBytes, + this.imageBytesFuture, + this.file, + }); + + io.Image? image; + Uint8List? imageBytes; + Future? imageBytesFuture; + File? file; + + Future getBytes() async { + if (imageBytes != null) { + return imageBytes; + } + if (imageBytesFuture != null) { + return imageBytesFuture; + } + if (file != null) { + return file!.readAsBytes(); + } + if (image == null) return null; + final img = await image!.toByteData(format: io.ImageByteFormat.png); + if (img == null) { + Log.error('Got no image'); + return null; + } + return imageBytes = img.buffer.asUint8List(); + } +} + +class ScreenshotController { + ScreenshotController() { + _containerKey = GlobalKey(); + } + late GlobalKey _containerKey; + + Future capture({double? pixelRatio}) async { + try { + final findRenderObject = _containerKey.currentContext?.findRenderObject(); + if (findRenderObject == null) { + return null; + } + final boundary = findRenderObject as RenderRepaintBoundary; + final context = _containerKey.currentContext; + var tmpPixelRatio = pixelRatio; + if (tmpPixelRatio == null) { + if (context != null && context.mounted) { + tmpPixelRatio = + tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio; + } + } + final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1); + return ScreenshotImage(image: image); + } catch (e) { + Log.error(e); + } + return null; + } +} + +class Screenshot extends StatefulWidget { + const Screenshot({ + required this.child, + required this.controller, + super.key, + }); + final Widget? child; + final ScreenshotController controller; + + @override + State createState() { + return ScreenshotState(); + } +} + +class ScreenshotState extends State with TickerProviderStateMixin { + late ScreenshotController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller; + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + key: _controller._containerKey, + child: widget.child, + ); + } +} diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index bba473f..5d9d4bb 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -1,6 +1,6 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:screenshot/screenshot.dart'; +import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 25f299b..82f5f27 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; - import 'package:camera/camera.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; @@ -19,6 +18,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.dart'; +import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart'; @@ -316,7 +316,6 @@ class _CameraPreviewViewState extends State { Future takePicture() async { if (_sharePreviewIsShown || _isVideoRecording) return; - late Future imageBytes; setState(() { _sharePreviewIsShown = true; @@ -345,10 +344,10 @@ class _CameraPreviewViewState extends State { return; } - imageBytes = mc.screenshotController + final image = await mc.screenshotController .capture(pixelRatio: MediaQuery.of(context).devicePixelRatio); - if (await pushMediaEditor(imageBytes, null)) { + if (await pushMediaEditor(image, null)) { return; } setState(() { @@ -357,7 +356,7 @@ class _CameraPreviewViewState extends State { } Future pushMediaEditor( - Future? imageBytes, + ScreenshotImage? imageBytes, File? videoFilePath, { bool sharedFromGallery = false, MediaType? mediaType, @@ -478,7 +477,7 @@ class _CameraPreviewViewState extends State { Log.info('Picket from gallery: ${pickedFile.path}'); File? videoFilePath; - Future? imageBytes; + ScreenshotImage? image; MediaType? mediaType; final isImage = @@ -487,13 +486,13 @@ class _CameraPreviewViewState extends State { if (pickedFile.name.contains('.gif')) { mediaType = MediaType.gif; } - imageBytes = pickedFile.readAsBytes(); + image = ScreenshotImage(imageBytesFuture: pickedFile.readAsBytes()); } else { videoFilePath = File(pickedFile.path); } await pushMediaEditor( - imageBytes, + image, videoFilePath, sharedFromGallery: true, mediaType: mediaType, diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 8250a53..63ac0b0 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -5,13 +5,13 @@ import 'package:drift/drift.dart' show Value; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; -import 'package:screenshot/screenshot.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/qr.dart'; +import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart'; diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index f1984a5..89071d8 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; @@ -15,7 +16,7 @@ class SaveToGalleryButton extends StatefulWidget { required this.mediaService, super.key, }); - final Future Function() storeImageAsOriginal; + final Future Function() storeImageAsOriginal; final bool displayButtonLabel; final MediaFileService mediaService; final bool isLoading; diff --git a/lib/src/views/camera/image_editor/layers/draw_layer.dart b/lib/src/views/camera/image_editor/layers/draw_layer.dart index a04f753..4207c39 100644 --- a/lib/src/views/camera/image_editor/layers/draw_layer.dart +++ b/lib/src/views/camera/image_editor/layers/draw_layer.dart @@ -3,8 +3,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hand_signature/signature.dart'; // ignore: implementation_imports import 'package:hand_signature/src/utils.dart'; -import 'package:screenshot/screenshot.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 8d19205..f0894cd 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -6,7 +6,6 @@ import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:screenshot/screenshot.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -15,6 +14,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.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/utils/screenshot.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart'; @@ -33,14 +33,15 @@ List undoLayers = []; List removedLayers = []; class ShareImageEditorView extends StatefulWidget { - const ShareImageEditorView( - {required this.sharedFromGallery, - required this.mediaFileService, - super.key, - this.imageBytesFuture, - this.sendToGroup, - this.mainCameraController}); - final Future? imageBytesFuture; + const ShareImageEditorView({ + required this.sharedFromGallery, + required this.mediaFileService, + super.key, + this.imageBytesFuture, + this.sendToGroup, + this.mainCameraController, + }); + final ScreenshotImage? imageBytesFuture; final Group? sendToGroup; final bool sharedFromGallery; final MediaFileService mediaFileService; @@ -84,9 +85,11 @@ class _ShareImageEditorView extends State { loadImage(widget.imageBytesFuture!); } else { if (widget.mediaFileService.tempPath.existsSync()) { - loadImage(widget.mediaFileService.tempPath.readAsBytes()); + loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath)); } else if (widget.mediaFileService.originalPath.existsSync()) { - loadImage(widget.mediaFileService.originalPath.readAsBytes()); + loadImage( + ScreenshotImage(file: widget.mediaFileService.originalPath), + ); } } } @@ -383,11 +386,11 @@ class _ShareImageEditorView extends State { } } - Future getEditedImageBytes() async { + Future getEditedImageBytes() async { if (layers.length == 1) { if (layers.first is BackgroundLayerData) { final image = (layers.first as BackgroundLayerData).image.bytes; - return image; + return ScreenshotImage(imageBytes: image); } } @@ -412,22 +415,31 @@ class _ShareImageEditorView extends State { return image; } - Future storeImageAsOriginal() async { + Future storeImageAsOriginal() async { if (mediaService.overlayImagePath.existsSync()) { mediaService.overlayImagePath.deleteSync(); } if (mediaService.tempPath.existsSync()) { mediaService.tempPath.deleteSync(); } + if (mediaService.originalPath.existsSync()) { + mediaService.originalPath.deleteSync(); + } + var bytes = imageBytes; if (media.type == MediaType.gif) { mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); } else { - final imageBytes = await getEditedImageBytes(); - if (imageBytes == null) return false; + final image = await getEditedImageBytes(); + if (image == null) return null; + bytes = await image.getBytes(); + if (bytes == null) { + Log.error('imageBytes are empty'); + return null; + } if (media.type == MediaType.image || media.type == MediaType.gif) { - mediaService.originalPath.writeAsBytesSync(imageBytes); + mediaService.originalPath.writeAsBytesSync(bytes!); } else if (media.type == MediaType.video) { - mediaService.overlayImagePath.writeAsBytesSync(imageBytes); + mediaService.overlayImagePath.writeAsBytesSync(bytes!); } else { Log.error('MediaType not supported: ${media.type}'); } @@ -447,12 +459,11 @@ class _ShareImageEditorView extends State { .renameSync(MediaFileService(mediaFile).storedPath.path); } } - return true; + return bytes; } - Future loadImage(Future imageBytesFuture) async { - imageBytes = await imageBytesFuture; - + Future loadImage(ScreenshotImage imageBytesFuture) async { + imageBytes = await imageBytesFuture.getBytes(); // store this image so it can be used as a draft in case the app is restarted mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); @@ -486,18 +497,18 @@ class _ShareImageEditorView extends State { sendingOrLoadingImage = true; }); - await storeImageAsOriginal(); - if (!context.mounted) return; // Insert media file into the messages database and start uploading process in the background - await insertMediaFileInMessagesTable( - mediaService, - [widget.sendToGroup!.groupId], + unawaited( + insertMediaFileInMessagesTable( + mediaService, + [widget.sendToGroup!.groupId], + storeImageAsOriginal(), + ), ); if (context.mounted) { - // ignore: use_build_context_synchronously Navigator.pop(context, true); } } diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 44ef8c9..6ee9eed 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/groups.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; @@ -26,7 +28,7 @@ class ShareImageView extends StatefulWidget { }); final HashSet selectedGroupIds; final void Function(String, bool) updateSelectedGroupIds; - final Future? mediaStoreFuture; + final Future? mediaStoreFuture; final MediaFileService mediaFileService; @override @@ -41,6 +43,7 @@ class _ShareImageView extends State { bool sendingImage = false; bool mediaStoreFutureReady = false; + Uint8List? _imageBytes; bool hideArchivedUsers = true; final TextEditingController searchUserName = TextEditingController(); late StreamSubscription> allGroupSub; @@ -63,10 +66,9 @@ class _ShareImageView extends State { Future initAsync() async { if (widget.mediaStoreFuture != null) { - await widget.mediaStoreFuture; + _imageBytes = await widget.mediaStoreFuture; } mediaStoreFutureReady = true; - // unawaited(startBackgroundMediaUpload(widget.mediaFileService)); if (!mounted) return; setState(() {}); } @@ -235,12 +237,31 @@ class _ShareImageView extends State { ), ), floatingActionButton: SizedBox( - height: 120, + height: 148, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( + child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + if (widget.mediaFileService.mediaFile.type == MediaType.image && + _imageBytes != null && + gUser.showShowImagePreviewWhenSending) + SizedBox( + height: 100, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: + Border.all(color: context.color.primary, width: 3), + color: context.color.primary, + borderRadius: BorderRadius.circular(10), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(7), + child: Image.memory(_imageBytes!), + ), + ), + ), FilledButton.icon( icon: !mediaStoreFutureReady || sendingImage ? SizedBox( @@ -265,6 +286,7 @@ class _ShareImageView extends State { await insertMediaFileInMessagesTable( widget.mediaFileService, widget.selectedGroupIds.toList(), + null, ); if (context.mounted) { @@ -288,7 +310,7 @@ class _ShareImageView extends State { ), ), label: Text( - context.lang.shareImagedEditorSendImage, + '${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})', style: const TextStyle(fontSize: 17), ), ), diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart index 24d252a..cf0a1bb 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -151,6 +151,7 @@ class _MessageInputState extends State { await insertMediaFileInMessagesTable( mediaFileService, [widget.group.groupId], + null, ); } diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index 4bbfda3..95485ba 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -174,7 +174,8 @@ class _MessageSendStateIconState extends State { ); }; } - if (mediaFile.uploadState == UploadState.preprocessing) { + if (mediaFile.uploadState == UploadState.preprocessing || + mediaFile.uploadState == UploadState.initialized) { text = context.lang.inProcess; } } diff --git a/lib/src/views/settings/appearance.view.dart b/lib/src/views/settings/appearance.view.dart index 8f8c338..4ea83cd 100644 --- a/lib/src/views/settings/appearance.view.dart +++ b/lib/src/views/settings/appearance.view.dart @@ -92,6 +92,16 @@ class _AppearanceViewState extends State { }); } + Future toggleShowImagePreviewWhenSending() async { + await updateUserdata((u) { + u.showShowImagePreviewWhenSending = !u.showShowImagePreviewWhenSending; + return u; + }); + setState(() { + // gUser + }); + } + @override Widget build(BuildContext context) { final selectedTheme = context.watch().themeMode; @@ -127,6 +137,14 @@ class _AppearanceViewState extends State { onChanged: (a) => toggleStartWithCameraOpen(), ), ), + ListTile( + title: Text(context.lang.showImagePreviewWhenSending), + onTap: toggleShowImagePreviewWhenSending, + trailing: Switch( + value: gUser.showShowImagePreviewWhenSending, + onChanged: (a) => toggleShowImagePreviewWhenSending(), + ), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 183f931..f8a222b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -882,10 +882,9 @@ packages: hand_signature: dependency: "direct main" description: - name: hand_signature - sha256: "05b40d3b2d1885a5dda126f26db386660aa46e497b63c96feb91d3198a667eea" - url: "https://pub.dev" - source: hosted + path: "dependencies/hand_signature" + relative: true + source: path version: "3.1.0+2" hashlib: dependency: "direct main" @@ -1532,14 +1531,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" - screenshot: - dependency: "direct main" - description: - name: screenshot - sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" - url: "https://pub.dev" - source: hosted - version: "3.0.0" scrollable_positioned_list: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c7f18c8..8b16d26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,13 +78,11 @@ dependencies: gal: ^2.3.1 get: ^4.7.2 google_mlkit_barcode_scanning: ^0.14.1 - hand_signature: ^3.0.3 image: ^4.3.0 no_screenshot: ^0.3.1 permission_handler: ^12.0.0+1 provider: ^6.1.2 restart_app: ^1.3.2 - screenshot: ^3.0.0 sentry_flutter: ^9.8.0 app_links: ^7.0.0 in_app_purchase: ^3.2.3 @@ -101,6 +99,7 @@ dependencies: mutex: ^3.1.0 introduction_screen: ^4.0.0 qr_flutter: ^4.1.0 + hand_signature: ^3.0.3 dependency_overrides: dots_indicator: @@ -123,6 +122,8 @@ dependency_overrides: path: ./dependencies/adaptive_number ed25519_edwards: path: ./dependencies/ed25519_edwards + hand_signature: + path: ./dependencies/hand_signature hashlib_codecs: path: ./dependencies/hashlib_codecs optional: From 987a55dc65cda433e28067268062e3fd20f4ee87 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 21:11:26 +0100 Subject: [PATCH 07/14] fix analyzer --- lib/src/views/camera/share_image_editor_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index f0894cd..d8ed0dd 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -437,9 +437,9 @@ class _ShareImageEditorView extends State { return null; } if (media.type == MediaType.image || media.type == MediaType.gif) { - mediaService.originalPath.writeAsBytesSync(bytes!); + mediaService.originalPath.writeAsBytesSync(bytes); } else if (media.type == MediaType.video) { - mediaService.overlayImagePath.writeAsBytesSync(bytes!); + mediaService.overlayImagePath.writeAsBytesSync(bytes); } else { Log.error('MediaType not supported: ${media.type}'); } From 11aa4c42028b12b1609c370032dd2eecc496ef24 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 21:46:48 +0100 Subject: [PATCH 08/14] fix #351 --- .../views/camera/share_image_editor_view.dart | 105 +++++++++++++----- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index d8ed0dd..2fe6dbd 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:drift/drift.dart' show Value; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -134,6 +135,82 @@ class _ShareImageEditorView extends State { setState(() {}); } + Future _setMaxShowTime(int? maxShowTime) async { + await mediaService.setDisplayLimit(maxShowTime); + if (!mounted) return; + setState(() {}); + await updateUserdata((user) { + user.defaultShowTime = maxShowTime; + return user; + }); + } + + Future _setImageDisplayTime() async { + if (media.type == MediaType.video) { + await mediaService.setDisplayLimit( + (media.displayLimitInMilliseconds == null) ? 0 : null, + ); + if (!mounted) return; + setState(() {}); + return; + } + + final options = [ + 1000, + 2000, + 3000, + 4000, + 5000, + 6000, + 7000, + 8000, + 9000, + 10000, + 15000, + 20000, + null, + ]; + + var initialItem = options.length - 1; + if (media.displayLimitInMilliseconds != null) { + initialItem = options.indexOf(media.displayLimitInMilliseconds); + if (initialItem == -1) { + initialItem = options.length - 1; + } + } + + await showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => Container( + height: 350, + padding: const EdgeInsets.only(top: 6), + margin: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: SafeArea( + top: false, + child: CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + itemExtent: 32, + scrollController: FixedExtentScrollController( + initialItem: initialItem, + ), + onSelectedItemChanged: (int selectedItem) { + _setMaxShowTime(options[selectedItem]); + }, + children: options.map((e) { + return Center( + child: Text(e == null ? '∞' : '${e ~/ 1000}s'), + ); + }).toList(), + ), + ), + ), + ); + } + List get actionsAtTheRight { if (layers.isNotEmpty && layers.last.isEditing && @@ -205,33 +282,7 @@ class _ShareImageEditorView extends State { : Icons.repeat_one_rounded : Icons.timer_outlined, tooltipText: context.lang.protectAsARealTwonly, - onPressed: () async { - if (media.type == MediaType.video) { - await mediaService.setDisplayLimit( - (media.displayLimitInMilliseconds == null) ? 0 : null, - ); - if (!mounted) return; - setState(() {}); - return; - } - int? maxShowTime; - if (media.displayLimitInMilliseconds == null) { - maxShowTime = 1000; - } else if (media.displayLimitInMilliseconds == 1000) { - maxShowTime = 5000; - } else if (media.displayLimitInMilliseconds == 5000) { - maxShowTime = 12000; - } else if (media.displayLimitInMilliseconds == 12000) { - maxShowTime = 20000; - } - await mediaService.setDisplayLimit(maxShowTime); - if (!mounted) return; - setState(() {}); - await updateUserdata((user) { - user.defaultShowTime = maxShowTime; - return user; - }); - }, + onPressed: _setImageDisplayTime, ), ), if (media.type == MediaType.video) From 6dc9aa10bc9f59f877e7f44f8e44935cda106a44 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 22:08:39 +0100 Subject: [PATCH 09/14] fix #350 --- lib/app.dart | 1 + lib/globals.dart | 6 ++++-- lib/src/localization/app_de.arb | 3 ++- lib/src/localization/app_en.arb | 3 ++- .../generated/app_localizations.dart | 6 ++++++ .../generated/app_localizations_de.dart | 5 +++++ .../generated/app_localizations_en.dart | 5 +++++ .../main_camera_controller.dart | 17 ++++++++++++++++- 8 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 7831a05..fa3a598 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -94,6 +94,7 @@ class _AppState extends State with WidgetsBindingObserver { listenable: context.watch(), builder: (BuildContext context, Widget? child) { return MaterialApp( + scaffoldMessengerKey: globalRootScaffoldMessengerKey, restorationScopeId: 'app', localizationsDelegates: const [ AppLocalizations.delegate, diff --git a/lib/globals.dart b/lib/globals.dart index 274507b..6196b6f 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,6 +1,5 @@ -import 'dart:ui'; - import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/api.service.dart'; @@ -37,3 +36,6 @@ bool globalAllowErrorTrackingViaSentry = false; late String globalApplicationCacheDirectory; late String globalApplicationSupportDirectory; + +final GlobalKey globalRootScaffoldMessengerKey = + GlobalKey(); diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 8cb4d76..1361dac 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -458,5 +458,6 @@ "couldNotVerifyUsername": "{username} konnte nicht verifiziert werden", "linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!", "startWithCameraOpen": "Mit geöffneter Kamera starten", - "showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen" + "showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen", + "verifiedPublicKey": "Der öffentliche Schlüssel von {username} wurde überprüft und ist gültig." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index ebc9f7f..96ce280 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -488,5 +488,6 @@ "couldNotVerifyUsername": "Could not verify {username}", "linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!", "startWithCameraOpen": "Start with camera open", - "showImagePreviewWhenSending": "Display image preview when selecting recipients" + "showImagePreviewWhenSending": "Display image preview when selecting recipients", + "verifiedPublicKey": "The public key of {username} has been verified and is valid." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 8d497c3..d337892 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2851,6 +2851,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Display image preview when selecting recipients'** String get showImagePreviewWhenSending; + + /// No description provided for @verifiedPublicKey. + /// + /// In en, this message translates to: + /// **'The public key of {username} has been verified and is valid.'** + String verifiedPublicKey(Object username); } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 49ab691..61bae51 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1577,4 +1577,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get showImagePreviewWhenSending => 'Bildvorschau bei der Auswahl von Empfängern anzeigen'; + + @override + String verifiedPublicKey(Object username) { + return 'Der öffentliche Schlüssel von $username wurde überprüft und ist gültig.'; + } } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 064d769..04d56d9 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1567,4 +1567,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get showImagePreviewWhenSending => 'Display image preview when selecting recipients'; + + @override + String verifiedPublicKey(Object username) { + return 'The public key of $username has been verified and is valid.'; + } } diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 63ac0b0..6c30d5a 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -2,14 +2,16 @@ import 'dart:io'; import 'package:camera/camera.dart'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; @@ -219,6 +221,19 @@ class MainCameraController { ); } await HapticFeedback.heavyImpact(); + if (verificationOk) { + globalRootScaffoldMessengerKey.currentState?.showSnackBar( + SnackBar( + content: Text( + globalRootScaffoldMessengerKey.currentContext?.lang + .verifiedPublicKey( + getContactDisplayName(contact)) ?? + '', + ), + duration: const Duration(seconds: 6), + ), + ); + } } } } else { From b093a7acdbcaef1a96f86139240ba2e1663acda9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 22:43:24 +0100 Subject: [PATCH 10/14] fix #354 --- lib/src/services/mediafiles/mediafile.service.dart | 2 +- lib/src/views/camera/share_image_editor_view.dart | 4 +++- lib/src/views/memories/memories_photo_slider.view.dart | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index b8a6c50..0d89fa1 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -219,7 +219,7 @@ class MediaFileService { await tempPath.copy(storedPath.path); } else { Log.error( - 'Could not store image neither as tempPath does not exists.', + 'Could not store image neither as ${tempPath.path} does not exists.', ); } unawaited(createThumbnail()); diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 2fe6dbd..071b9d3 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -474,7 +474,9 @@ class _ShareImageEditorView extends State { mediaService.tempPath.deleteSync(); } if (mediaService.originalPath.existsSync()) { - mediaService.originalPath.deleteSync(); + if (media.type != MediaType.video) { + mediaService.originalPath.deleteSync(); + } } var bytes = imageBytes; if (media.type == MediaType.gif) { diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index b2016d5..13cea18 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -133,7 +133,7 @@ class _MemoriesPhotoSliderViewState extends State { } orgMediaService.storedPath - .copySync(newMediaService.tempPath.path); + .copySync(newMediaService.originalPath.path); if (!context.mounted) return; From 0984eaf3477758ca12072e017ee6c48eb32946ef Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 26 Dec 2025 22:49:45 +0100 Subject: [PATCH 11/14] fix #355 --- .../camera_preview_controller_view.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 82f5f27..1009857 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -28,6 +28,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector. import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/home.view.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -639,10 +640,10 @@ class _CameraPreviewViewState extends State { if (_galleryLoadedImageIsShown) Center( child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 1, + height: 60, + width: 60, + child: ThreeRotatingDots( + size: 40, color: context.color.primary, ), ), From 230809290a52152ccf82e53774f7eb18138889fe Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 27 Dec 2025 15:09:36 +0100 Subject: [PATCH 12/14] fixes #340 and continue with #333 --- android/app/src/main/AndroidManifest.xml | 18 +- dependencies | 2 +- ios/Podfile | 7 + ios/Podfile.lock | 14 +- ios/Runner.xcodeproj/project.pbxproj | 257 +++++++- ios/Runner/AppDelegate.swift | 12 + ios/Runner/Info.plist | 16 +- ios/Runner/RunnerDebug.entitlements | 20 + .../Base.lproj/MainInterface.storyboard | 24 + .../FSIShareViewController.swift | 553 ++++++++++++++++++ ios/ShareExtension/Info.plist | 35 ++ .../ShareExtensionDebug.entitlements | 10 + ios/ShareExtension/ShareViewController.swift | 3 + lib/src/services/intent/links.intent.dart | 180 ++++++ .../main_camera_controller.dart | 3 +- lib/src/views/home.view.dart | 119 +--- pubspec.lock | 52 +- pubspec.yaml | 61 +- 18 files changed, 1219 insertions(+), 167 deletions(-) create mode 100644 ios/Runner/RunnerDebug.entitlements create mode 100644 ios/ShareExtension/Base.lproj/MainInterface.storyboard create mode 100644 ios/ShareExtension/FSIShareViewController.swift create mode 100644 ios/ShareExtension/Info.plist create mode 100644 ios/ShareExtension/ShareExtensionDebug.entitlements create mode 100644 ios/ShareExtension/ShareViewController.swift create mode 100644 lib/src/services/intent/links.intent.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 44a350d..bc0b3a7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,6 +33,16 @@ + + + + + + + + + + @@ -46,19 +56,11 @@ android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService" tools:node="remove"> - - - - - diff --git a/dependencies b/dependencies index 83475a9..7930d97 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit 83475a912851acb6a718ea32a6f0f754d64a50d8 +Subproject commit 7930d9727019344238297d810661bc3e8f724c37 diff --git a/ios/Podfile b/ios/Podfile index a5993b4..f30e30f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -43,6 +43,13 @@ target 'Runner' do target 'RunnerTests' do inherit! :search_paths end + + # Share Extension is name of Extension which you created which is in this case 'Share Extension' + target 'ShareExtension' do + inherit! :search_paths + # flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + end + end post_install do |installer| diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a6414db..301b4b9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -122,6 +122,8 @@ PODS: - flutter_secure_storage_darwin (10.0.0): - Flutter - FlutterMacOS + - flutter_sharing_intent (1.0.1): + - Flutter - flutter_volume_controller (0.0.1): - Flutter - gal (1.0.0): @@ -260,7 +262,7 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - - "no_screenshot (0.0.1+4)": + - no_screenshot (0.3.2-beta.3): - Flutter - ScreenProtectorKit (~> 1.3.1) - objective_c (0.0.1): @@ -350,6 +352,7 @@ DEPENDENCIES: - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`) @@ -441,6 +444,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage_darwin: :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + flutter_sharing_intent: + :path: ".symlinks/plugins/flutter_sharing_intent/ios" flutter_volume_controller: :path: ".symlinks/plugins/flutter_volume_controller/ios" gal: @@ -507,7 +512,8 @@ SPEC CHECKSUMS: flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb - flutter_secure_storage_darwin: ce237a8775b39723566dc72571190a3769d70468 + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb gal: baecd024ebfd13c441269ca7404792a7152fde89 google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159 @@ -529,7 +535,7 @@ SPEC CHECKSUMS: MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e + no_screenshot: 89e778ede9f1e39cc3fb9404d782a42712f2a0b2 objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 @@ -551,6 +557,6 @@ SPEC CHECKSUMS: url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a -PODFILE CHECKSUM: c0c524475498435108850efecde62ba98e081c25 +PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fb716f3..5739f2b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 05CF222065FC24670B05B6D0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */; }; 06AA21445BEAF2C45DC9DCDF /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 30EBDD0F93DC44E774F3B785 /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E190E82D9973B318A389650B /* Pods_ShareExtension.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -19,6 +20,7 @@ CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */; }; D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D21FCEA42D9F2B750088701D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25D4D1D2EF626E30029F805 /* StoreKit.framework */; }; + D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ @@ -37,6 +39,13 @@ remoteGlobalIDString = D21FCEA32D9F2B750088701D; remoteInfo = NotificationService; }; + D25D4D782EFF41DB0029F805 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = D25D4D6F2EFF41DB0029F805; + remoteInfo = ShareExtension; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -56,6 +65,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */, D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; @@ -67,10 +77,12 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1581CC44342D555EFB889768 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 15CEF849B61A620CFB2DC5F1 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 35366FD433E0EFC6EF19A452 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = ""; }; + 39FB86A38393489D58A01B0B /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 4D78471482626812FE2468E9 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = ""; }; 6EB462F87F0A23758713308F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; @@ -87,11 +99,15 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A22BD564F16069E5FCB60767 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; B3B27B7FBEEA31DB7793A0C2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; D21FCEA42D9F2B750088701D /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D2265DD42D920142000D99BB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; D25D4D1D2EF626E30029F805 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + D25D4D702EFF41DB0029F805 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = ""; }; @@ -105,6 +121,13 @@ ); target = D21FCEA32D9F2B750088701D /* NotificationService */; }; + D25D4D7E2EFF41DB0029F805 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = D25D4D6F2EFF41DB0029F805 /* ShareExtension */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -116,6 +139,14 @@ path = NotificationService; sourceTree = ""; }; + D25D4D712EFF41DB0029F805 /* ShareExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D25D4D7E2EFF41DB0029F805 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */, + ); + path = ShareExtension; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -144,6 +175,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D25D4D6D2EFF41DB0029F805 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 30EBDD0F93DC44E774F3B785 /* Pods_ShareExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -172,6 +211,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, D21FCEA52D9F2B750088701D /* NotificationService */, + D25D4D712EFF41DB0029F805 /* ShareExtension */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */, @@ -186,6 +226,7 @@ 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, D21FCEA42D9F2B750088701D /* NotificationService.appex */, + D25D4D702EFF41DB0029F805 /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -193,6 +234,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */, D2265DD42D920142000D99BB /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -213,6 +255,7 @@ A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */, EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */, DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */, + E190E82D9973B318A389650B /* Pods_ShareExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -229,6 +272,9 @@ 4D78471482626812FE2468E9 /* Pods-NotificationService.debug.xcconfig */, 35366FD433E0EFC6EF19A452 /* Pods-NotificationService.release.xcconfig */, F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */, + 15CEF849B61A620CFB2DC5F1 /* Pods-ShareExtension.debug.xcconfig */, + A22BD564F16069E5FCB60767 /* Pods-ShareExtension.release.xcconfig */, + 39FB86A38393489D58A01B0B /* Pods-ShareExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -274,6 +320,7 @@ ); dependencies = ( D21FCEAA2D9F2B750088701D /* PBXTargetDependency */, + D25D4D792EFF41DB0029F805 /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -301,6 +348,27 @@ productReference = D21FCEA42D9F2B750088701D /* NotificationService.appex */; productType = "com.apple.product-type.app-extension"; }; + D25D4D6F2EFF41DB0029F805 /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = D25D4D7F2EFF41DB0029F805 /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 627F39EA1643E08048D23996 /* [CP] Check Pods Manifest.lock */, + D25D4D6C2EFF41DB0029F805 /* Sources */, + D25D4D6D2EFF41DB0029F805 /* Frameworks */, + D25D4D6E2EFF41DB0029F805 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + D25D4D712EFF41DB0029F805 /* ShareExtension */, + ); + name = ShareExtension; + productName = ShareExtension; + productReference = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -308,7 +376,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1620; + LastSwiftUpdateCheck = 2610; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -323,6 +391,9 @@ D21FCEA32D9F2B750088701D = { CreatedOnToolsVersion = 16.2; }; + D25D4D6F2EFF41DB0029F805 = { + CreatedOnToolsVersion = 26.1.1; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -341,6 +412,7 @@ 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, D21FCEA32D9F2B750088701D /* NotificationService */, + D25D4D6F2EFF41DB0029F805 /* ShareExtension */, ); }; /* End PBXProject section */ @@ -372,6 +444,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D25D4D6E2EFF41DB0029F805 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -452,6 +531,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; + 627F39EA1643E08048D23996 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -533,6 +634,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D25D4D6C2EFF41DB0029F805 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -546,6 +654,11 @@ target = D21FCEA32D9F2B750088701D /* NotificationService */; targetProxy = D21FCEA92D9F2B750088701D /* PBXContainerItemProxy */; }; + D25D4D792EFF41DB0029F805 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D25D4D6F2EFF41DB0029F805 /* ShareExtension */; + targetProxy = D25D4D782EFF41DB0029F805 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -631,6 +744,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = group.eu.twonly.shareIntent; DEVELOPMENT_TEAM = CN332ZUGRP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -827,8 +941,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = group.eu.twonly.shareIntent; DEVELOPMENT_TEAM = CN332ZUGRP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -863,6 +978,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = group.eu.twonly.shareIntent; DEVELOPMENT_TEAM = CN332ZUGRP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -1004,6 +1120,133 @@ }; name = Profile; }; + D25D4D7B2EFF41DB0029F805 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 15CEF849B61A620CFB2DC5F1 /* Pods-ShareExtension.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtensionDebug.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.eu.twonly.shareIntent; + DEVELOPMENT_TEAM = CN332ZUGRP; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = eu.twonly.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D25D4D7C2EFF41DB0029F805 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A22BD564F16069E5FCB60767 /* Pods-ShareExtension.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.eu.twonly.shareIntent; + DEVELOPMENT_TEAM = CN332ZUGRP; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = eu.twonly.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D25D4D7D2EFF41DB0029F805 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 39FB86A38393489D58A01B0B /* Pods-ShareExtension.profile.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.eu.twonly.shareIntent; + DEVELOPMENT_TEAM = CN332ZUGRP; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = eu.twonly.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1047,6 +1290,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D25D4D7F2EFF41DB0029F805 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D25D4D7B2EFF41DB0029F805 /* Debug */, + D25D4D7C2EFF41DB0029F805 /* Release */, + D25D4D7D2EFF41DB0029F805 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index ae7cbe1..b36ae26 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -3,6 +3,7 @@ import Flutter import Foundation import UIKit import UserNotifications +import flutter_sharing_intent @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { @@ -14,6 +15,17 @@ import UserNotifications return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + + let sharingIntent = SwiftFlutterSharingIntentPlugin.instance + if sharingIntent.hasSameSchemePrefix(url: url) { + return sharingIntent.application(app, open: url, options: options) + } + + // Proceed url handling for other Flutter libraries like app_links + return super.application(app, open: url, options:options) + } + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d00d31e..8b4c63d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -87,9 +87,21 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - firebase_performance_collection_deactivated - + + AppGroupId + $(CUSTOM_GROUP_ID) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements new file mode 100644 index 0000000..dc9d4cc --- /dev/null +++ b/ios/Runner/RunnerDebug.entitlements @@ -0,0 +1,20 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:me.twonly.eu + + com.apple.security.application-groups + + group.eu.twonly.shareIntent + + keychain-access-groups + + $(AppIdentifierPrefix)eu.twonly.shared + + + diff --git a/ios/ShareExtension/Base.lproj/MainInterface.storyboard b/ios/ShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000..286a508 --- /dev/null +++ b/ios/ShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/ShareExtension/FSIShareViewController.swift b/ios/ShareExtension/FSIShareViewController.swift new file mode 100644 index 0000000..9a2e198 --- /dev/null +++ b/ios/ShareExtension/FSIShareViewController.swift @@ -0,0 +1,553 @@ +// SOURCE: https://github.com/bhagat-techind/flutter_sharing_intent/blob/main/example/ios/Share%20Extension/FSIShareViewController.swift + +// FSIShareViewController.swift +// Merged, optimized controller: uses RSI architecture with all FSI features preserved +// Uses model name `SharingFile` (same fields as SharedMediaFile) where `value` = path + +import AVFoundation +import MobileCoreServices +import Social +import UIKit +import UniformTypeIdentifiers + +public let kSchemePrefix = "SharingMedia" +public let kUserDefaultsKey = "SharingKey" +public let kUserDefaultsMessageKey = "SharingMessageKey" +public let kAppGroupIdKey = "AppGroupId" +public let kAppChannel = "flutter_sharing_intent" + +@available(swift, introduced: 5.0) +open class FSIShareViewController: SLComposeServiceViewController { + // MARK: - Config + private(set) var hostAppBundleIdentifier: String = "" + private(set) var appGroupId: String = "" + + // Results + private var sharedMedia: [SharingFile] = [] + + // Debug + private let debugLogs = false + + // MARK: - Lifecycle + open override func viewDidLoad() { + super.viewDidLoad() + loadIds() + } + + open override func isContentValid() -> Bool { + return true + } + + open override func didSelectPost() { + if self.sharedMedia.isEmpty { + if let text = self.contentText, !text.isEmpty { + self.sharedMedia.append( + SharingFile(value: text, thumbnail: nil, duration: nil, type: .text) + ) + self.saveAndRedirect(message: text) + return + } + self.completeAndExit() + } else { + self.saveAndRedirect() + } + } + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // Process attachments automatically on appear like original FSI + processAttachments() + } + + // MARK: - Load Ids + private func loadIds() { + let shareExtId = Bundle.main.bundleIdentifier ?? "" + if let idx = shareExtId.lastIndex(of: ".") { + hostAppBundleIdentifier = String(shareExtId[.. media, else fallback to complete + if !self.sharedMedia.isEmpty { + self.saveAndRedirect() + } else { + print("FSIShare: No shared media → stopping.") + self.completeAndExit() + } + } + } + + // MARK: - Individual handlers (preserve FSI behavior) + private func handleTextItem(data: NSSecureCoding?, index: Int, total: Int) { + if let s = data as? String { + sharedMedia.append(SharingFile(value: s, thumbnail: nil, duration: nil, type: .text)) + } else if let url = data as? URL { + sharedMedia.append(SharingFile(value: url.absoluteString, thumbnail: nil, duration: nil, type: .url)) + } + + } + + private func handleUrlItem(data: NSSecureCoding?, index: Int, total: Int) { + if let url = data as? URL { + sharedMedia.append(SharingFile(value: url.absoluteString, thumbnail: nil, duration: nil, type: .url)) + } else if let s = data as? String { + sharedMedia.append(SharingFile(value: s, thumbnail: nil, duration: nil, type: .text)) + } + + } + + private func handleImageItem(data: NSSecureCoding?, index: Int, total: Int) { + // data can be URL, UIImage, or Data + if let url = data as? URL { + let filename = getFileName(from: url, type: .image) + if let dst = containerURL()?.appendingPathComponent(filename) { + if copyFile(at: url, to: dst) { + sharedMedia.append(SharingFile(value: dst.absoluteString, mimeType: url.mimeType(), thumbnail: nil, duration: nil, type: .image)) + } + } + } else if let img = data as? UIImage { + if let saved = writeTempImage(img) { + sharedMedia.append(saved) + } + } else if let raw = data as? Data, let img = UIImage(data: raw) { + if let saved = writeTempImage(img) { + sharedMedia.append(saved) + } + } + + } + + private func handleVideoItem(data: NSSecureCoding?, index: Int, total: Int) { + if let url = data as? URL { + let filename = getFileName(from: url, type: .video) + if let dst = containerURL()?.appendingPathComponent(filename) { + if copyFile(at: url, to: dst) { + if let m = getSharedMediaFile(forVideo: dst) { + sharedMedia.append(m) + } + } + } + } + + } + + private func handleFileItem(data: NSSecureCoding?, index: Int, total: Int) { + if let url = data as? URL { + let filename = getFileName(from: url, type: .file) + if let dst = containerURL()?.appendingPathComponent(filename) { + if copyFile(at: url, to: dst) { + sharedMedia.append(SharingFile(value: dst.absoluteString, mimeType: url.mimeType(), thumbnail: nil, duration: nil, type: .file)) + } + } + } + else if let raw = data as? Data { + let filename = "File_\(UUID().uuidString)" + if let dst = containerURL()?.appendingPathComponent(filename) { + do { + try raw.write(to: dst) + sharedMedia.append(SharingFile(value: dst.absoluteString, mimeType: "application/octet-stream", thumbnail: nil, duration: nil, type: .file)) + } catch {} + } + } + + + } + + // MARK: - Helpers: write temp image + private func writeTempImage(_ image: UIImage) -> SharingFile? { + guard let container = containerURL() else { return nil } + let tempName = "TempImage_\(UUID().uuidString).png" + let dst = container.appendingPathComponent(tempName) + do { + if let d = image.pngData() { + try d.write(to: dst) + let decoded = dst.absoluteString.removingPercentEncoding ?? dst.absoluteString + return SharingFile(value: decoded, mimeType: "image/png", thumbnail: nil, duration: nil, type: .image) + } + } catch { + log("writeTempImage error: \(error)") + } + return nil + } + + + private func saveAndRedirect(message: String? = nil) { + let ud = UserDefaults(suiteName: appGroupId) + if !sharedMedia.isEmpty { + if let data = try? JSONEncoder().encode(sharedMedia) { + ud?.set(data, forKey: kUserDefaultsKey) + } + } + ud?.set(message, forKey: kUserDefaultsMessageKey) + ud?.synchronize() + redirectToHostApp() + } + + + private func redirectToHostApp() { + // kept for compatibility (RSI style) + loadIds() + // let raw = "\(kSchemePrefix)-\(hostAppBundleIdentifier):share" + let raw = "\(kSchemePrefix)-\(hostAppBundleIdentifier)://dataUrl=\(kUserDefaultsKey)" + guard let url = URL(string: raw.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? raw) else { completeAndExit(); return } + + var responder: UIResponder? = self + if #available(iOS 18.0, *) { + while responder != nil { + if let app = responder as? UIApplication { app.open(url, options: [:], completionHandler: nil) } + responder = responder?.next + } + } else { + let sel = sel_registerName("openURL:") + while responder != nil { + if responder?.responds(to: sel) ?? false { _ = responder?.perform(sel, with: url) } + responder = responder?.next + } + } + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + + // MARK: - File / thumbnail / metadata helpers + func getExtension(from url: URL, type: SharingFileType) -> String { + let parts = url.lastPathComponent.components(separatedBy: ".") + var ex: String? = nil + if parts.count > 1 { ex = parts.last } + if ex == nil { + switch type { + case .image: ex = "png" + case .video: ex = "mp4" + case .file: ex = "txt" + case .text: ex = "txt" + case .url: ex = "txt" + } + } + return ex ?? "bin" + } + + func getFileName(from url: URL, type: SharingFileType) -> String { + var name = url.lastPathComponent + if name.isEmpty { name = UUID().uuidString + "." + getExtension(from: url, type: type) } + return name + } + + func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { + do { + if FileManager.default.fileExists(atPath: dstURL.path) { try FileManager.default.removeItem(at: dstURL) } + try FileManager.default.copyItem(at: srcURL, to: dstURL) + return true + } catch { + log("copyFile error: \(error)") + return false + } + } + + private func getSharedMediaFile(forVideo: URL) -> SharingFile? { + let asset = AVAsset(url: forVideo) + let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() + let thumbnailPath = getThumbnailPath(for: forVideo) + + if FileManager.default.fileExists(atPath: thumbnailPath.path) { + return SharingFile(value: forVideo.absoluteString, mimeType: forVideo.mimeType(), thumbnail: thumbnailPath.absoluteString, duration: Int(duration), type: .video) + } + + let gen = AVAssetImageGenerator(asset: asset) + gen.appliesPreferredTrackTransform = true + gen.maximumSize = CGSize(width: 360, height: 360) + + // Use first second or zero + let time = CMTime(seconds: min(1.0, CMTimeGetSeconds(asset.duration)), preferredTimescale: 600) + do { + let cg = try gen.copyCGImage(at: time, actualTime: nil) + if let data = UIImage(cgImage: cg).jpegData(compressionQuality: 0.8) { + try data.write(to: thumbnailPath) + return SharingFile(value: forVideo.absoluteString, mimeType: forVideo.mimeType(), thumbnail: thumbnailPath.absoluteString, duration: Int(duration), type: .video) + } + } catch { + log("getSharedMediaFile thumbnail error: \(error)") + } + + // fallback + return SharingFile(value: forVideo.absoluteString, mimeType: forVideo.mimeType(), thumbnail: nil, duration: Int(duration), type: .video) + } + + private func getThumbnailPath(for url: URL) -> URL { + guard let container = containerURL() else { fatalError("App group not configured or missing") } + let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "=", with: "") + return container.appendingPathComponent("\(fileName).jpg") + } + + private func containerURL() -> URL? { + FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) + } + + private func completeAndExit() { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + + private func dismissWithError() { + log("[ERROR] Error loading data!") + let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .cancel) { _ in self.dismiss(animated: true, completion: nil) }) + present(alert, animated: true, completion: nil) + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + + private func writeTempFile(_ image: UIImage, to dstURL: URL) -> Bool { + do { + if FileManager.default.fileExists(atPath: dstURL.path) { try FileManager.default.removeItem(at: dstURL) } + let pngData = image.pngData() + try pngData?.write(to: dstURL) + return true + } catch (let error) { + log("writeTempFile error: \(error)") + return false + } + } + + private func saveToUserDefaults(data: [SharingFile]) { + let ud = UserDefaults(suiteName: appGroupId) + if let enc = try? JSONEncoder().encode(data) { ud?.set(enc, forKey: kUserDefaultsKey); ud?.synchronize() } + } + + // MARK: - Logging + private func log(_ s: String) { if debugLogs { print("[FSIShareVC] \(s)") } } + +} + +// MARK: - Extensions +extension URL { + func mimeType() -> String { + if #available(iOS 14.0, *) { + if let ut = UTType(filenameExtension: self.pathExtension), let m = ut.preferredMIMEType { return m } + } else { + if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, self.pathExtension as NSString, nil)?.takeRetainedValue() { + if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { return mimetype as String } + } + } + return "application/octet-stream" + } +} + +extension NSItemProvider { + var isImage: Bool { return hasItemConformingToTypeIdentifier(UType.image) } + var isMovie: Bool { return hasItemConformingToTypeIdentifier(UType.movie) } + var isText: Bool { + hasItemConformingToTypeIdentifier(UType.plainText) || hasItemConformingToTypeIdentifier(UType.text) + } + var isURL: Bool { return hasItemConformingToTypeIdentifier(UType.url) } + var isFile: Bool { return hasItemConformingToTypeIdentifier(UType.fileURL) } + var isData:Bool { return hasItemConformingToTypeIdentifier(UType.data) } + var isItem: Bool { hasItemConformingToTypeIdentifier(UType.item) } + +} + +extension Array { + subscript(safe index: UInt) -> Element? { return Int(index) < count ? self[Int(index)] : nil } +} + + +class SharingFile: Codable { + var value: String + var mimeType: String? + var thumbnail: String?; // video thumbnail + var duration: Int?; // video duration in milliseconds + var type: SharingFileType; + var message: String? // post message + + enum CodingKeys: String, CodingKey { + case value + case mimeType + case thumbnail + case duration + case type + case message + } + + init(value: String, mimeType: String? = nil, thumbnail: String?, duration: Int?, + type: SharingFileType, message: String?=nil) { + self.value = value + self.mimeType = mimeType + self.thumbnail = thumbnail + self.duration = duration + self.type = type + self.message = message + } + + // Debug method to print out SharedMediaFile details in the console + func toString() { + print("[SharingFile] \n\tvalue: \(self.value)\n\tthumbnail: \(self.thumbnail ?? "--" )\n\tduration: \(self.duration ?? 0)\n\ttype: \(self.type)\n\tmimeType: \(String(describing: self.mimeType))\n\tmessage: \(String(describing: self.message))") + } +} + + +enum SharingFileType: Int, Codable { + case text + case url + case image + case video + case file +} + +// Unified UTType → works on iOS 11–18 +enum UType { + static var image: String { + if #available(iOS 14.0, *) { + return UTType.image.identifier + } else { + return kUTTypeImage as String // old API + } + } + + static var movie: String { + if #available(iOS 14.0, *) { + return UTType.movie.identifier + } else { + return kUTTypeMovie as String + } + } + + + static var url: String { + if #available(iOS 14.0, *) { + return UTType.url.identifier + } else { + return kUTTypeURL as String + } + } + + static var fileURL: String { + if #available(iOS 14.0, *) { + return UTType.fileURL.identifier + } else { + return kUTTypeFileURL as String + } + } + + static var text: String { + if #available(iOS 14.0, *) { + return UTType.text.identifier + } else { + return kUTTypeText as String + } + } + + static var plainText: String { + if #available(iOS 14.0, *) { + return UTType.plainText.identifier + } else { + return kUTTypePlainText as String + } + } + + static var data: String { + if #available(iOS 14.0, *) { + return UTType.data.identifier + } else { + return kUTTypeData as String + } + } + + static var item: String { + if #available(iOS 14.0, *) { + return UTType.item.identifier + } else { + return kUTTypeItem as String + } + } +} diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist new file mode 100644 index 0000000..3e8521b --- /dev/null +++ b/ios/ShareExtension/Info.plist @@ -0,0 +1,35 @@ + + + + + AppGroupId + $(CUSTOM_GROUP_ID) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + NSExtension + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionAttributes + + PHSupportedMediaTypes + + Video + Image + + + NSExtensionActivationRule + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + + + + + diff --git a/ios/ShareExtension/ShareExtensionDebug.entitlements b/ios/ShareExtension/ShareExtensionDebug.entitlements new file mode 100644 index 0000000..bb03fc7 --- /dev/null +++ b/ios/ShareExtension/ShareExtensionDebug.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.eu.twonly.shareIntent + + + diff --git a/ios/ShareExtension/ShareViewController.swift b/ios/ShareExtension/ShareViewController.swift new file mode 100644 index 0000000..668bd61 --- /dev/null +++ b/ios/ShareExtension/ShareViewController.swift @@ -0,0 +1,3 @@ +class ShareViewController: FSIShareViewController { + +} \ No newline at end of file diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart new file mode 100644 index 0000000..f951695 --- /dev/null +++ b/lib/src/services/intent/links.intent.dart @@ -0,0 +1,180 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:flutter_sharing_intent/model/sharing_file.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/chats/add_new_user.view.dart'; +import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/public_profile.view.dart'; + +Future handleIntentUrl(BuildContext context, Uri uri) async { + if (!uri.scheme.startsWith('http')) return; + if (uri.host != 'me.twonly.eu') return; + if (uri.hasEmptyPath) return; + + final publicKey = uri.hasFragment ? uri.fragment : null; + final userPaths = uri.path.split('/'); + if (userPaths.length != 2) return; + final username = userPaths[1]; + + if (!context.mounted) return; + + if (username == gUser.username) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const PublicProfileView(); + }, + ), + ); + return; + } + + Log.info( + 'Opened via deep link!: username = $username public_key = ${uri.fragment}', + ); + final contacts = await twonlyDB.contactsDao.getContactsByUsername(username); + if (contacts.isEmpty) { + if (!context.mounted) return; + Uint8List? publicKeyBytes; + if (publicKey != null) { + publicKeyBytes = base64Url.decode(publicKey); + } + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return AddNewUserView( + username: username, + publicKey: publicKeyBytes, + ); + }, + ), + ); + } else if (publicKey != null) { + try { + final contact = contacts.first; + final storedPublicKey = await getPublicKeyFromContact(contact.userId); + final receivedPublicKey = base64Url.decode(publicKey); + if (storedPublicKey == null || + receivedPublicKey.isEmpty || + !context.mounted) { + return; + } + if (storedPublicKey.equals(receivedPublicKey)) { + if (!contact.verified) { + final markAsVerified = await showAlertDialog( + context, + context.lang.linkFromUsername(contact.username), + context.lang.linkFromUsernameLong, + customOk: context.lang.gotLinkFromFriend, + ); + if (markAsVerified) { + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + verified: Value(true), + ), + ); + } + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactView(contact.userId); + }, + ), + ); + } + } else { + await showAlertDialog( + context, + context.lang.couldNotVerifyUsername(contact.username), + context.lang.linkPubkeyDoesNotMatch, + customCancel: '', + ); + } + } catch (e) { + Log.warn(e); + } + } +} + +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, + gUser.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, + ), + ), + ); +} + +Future handleIntentSharedFile( + BuildContext context, + List files, +) async { + for (final file in files) { + if (file.value == null) { + Log.error( + 'Got shared media, but value is empty: getMediaStream ${file.mimeType}', + ); + continue; + } + Log.info('got file via intent ${file.type} ${file.value}'); + + switch (file.type) { + case SharedMediaType.URL: + await handleIntentUrl(context, Uri.parse(file.value!)); + case SharedMediaType.IMAGE: + var type = MediaType.image; + if (file.value!.endsWith('.gif')) { + type = MediaType.gif; + } + await handleIntentMediaFile(context, file.value!, type); + case SharedMediaType.VIDEO: + await handleIntentMediaFile(context, file.value!, MediaType.video); + // ignore: no_default_cases + default: + } + break; // only handle one file... + } +} diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 6c30d5a..c6543a7 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -227,7 +227,8 @@ class MainCameraController { content: Text( globalRootScaffoldMessengerKey.currentContext?.lang .verifiedPublicKey( - getContactDisplayName(contact)) ?? + getContactDisplayName(contact), + ) ?? '', ), duration: const Duration(seconds: 6), diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index bb065a4..b87ab11 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -1,29 +1,22 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; import 'package:app_links/app_links.dart'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; +import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/intent/links.intent.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; -import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; -import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; -import 'package:twonly/src/views/components/alert_dialog.dart'; -import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; -import 'package:twonly/src/views/public_profile.view.dart'; void Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; @@ -61,6 +54,7 @@ class HomeViewState extends State { final MainCameraController _mainCameraController = MainCameraController(); final PageController homeViewPageController = PageController(initialPage: 1); + late StreamSubscription> _intentStreamSub; late StreamSubscription _deepLinkSub; double buttonDiameter = 100; @@ -121,99 +115,21 @@ class HomeViewState extends State { // Subscribe to all events (initial link and further) _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { - if (!uri.scheme.startsWith('http')) return; - if (uri.host != 'me.twonly.eu') return; - if (uri.hasEmptyPath) return; + if (mounted) await handleIntentUrl(context, uri); + }); - final publicKey = uri.hasFragment ? uri.fragment : null; - final userPaths = uri.path.split('/'); - if (userPaths.length != 2) return; - final username = userPaths[1]; + _intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen( + (f) { + if (mounted) handleIntentSharedFile(context, f); + }, + // ignore: inference_failure_on_untyped_parameter + onError: (err) { + Log.error('getIntentDataStream error: $err'); + }, + ); - if (!mounted) return; - - if (username == gUser.username) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const PublicProfileView(); - }, - ), - ); - return; - } - - Log.info( - 'Opened via deep link!: username = $username public_key = ${uri.fragment}', - ); - final contacts = - await twonlyDB.contactsDao.getContactsByUsername(username); - if (contacts.isEmpty) { - if (!mounted) return; - Uint8List? publicKeyBytes; - if (publicKey != null) { - publicKeyBytes = base64Url.decode(publicKey); - } - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return AddNewUserView( - username: username, - publicKey: publicKeyBytes, - ); - }, - ), - ); - } else if (publicKey != null) { - try { - final contact = contacts.first; - final storedPublicKey = await getPublicKeyFromContact(contact.userId); - final receivedPublicKey = base64Url.decode(publicKey); - if (storedPublicKey == null || - receivedPublicKey.isEmpty || - !mounted) { - return; - } - if (storedPublicKey.equals(receivedPublicKey)) { - if (!contact.verified) { - final markAsVerified = await showAlertDialog( - context, - context.lang.linkFromUsername(contact.username), - context.lang.linkFromUsernameLong, - customOk: context.lang.gotLinkFromFriend, - ); - if (markAsVerified) { - await twonlyDB.contactsDao.updateContact( - contact.userId, - const ContactsCompanion( - verified: Value(true), - ), - ); - } - } else { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactView(contact.userId); - }, - ), - ); - } - } else { - await showAlertDialog( - context, - context.lang.couldNotVerifyUsername(contact.username), - context.lang.linkPubkeyDoesNotMatch, - customCancel: '', - ); - } - } catch (e) { - Log.warn(e); - } - } + FlutterSharingIntent.instance.getInitialSharing().then((f) { + if (mounted) handleIntentSharedFile(context, f); }); } @@ -222,6 +138,7 @@ class HomeViewState extends State { unawaited(selectNotificationStream.close()); disableCameraTimer?.cancel(); _mainCameraController.closeCamera(); + _intentStreamSub.cancel(); _deepLinkSub.cancel(); super.dispose(); } diff --git a/pubspec.lock b/pubspec.lock index f8a222b..ea1d1dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -742,29 +742,27 @@ packages: flutter_secure_storage: dependency: "direct main" description: - path: flutter_secure_storage - ref: a06ead81809c900e7fc421a30db0adf3b5919139 - resolved-ref: a06ead81809c900e7fc421a30db0adf3b5919139 - url: "https://github.com/juliansteenbakker/flutter_secure_storage.git" - source: git - version: "10.0.0-beta.4" + name: flutter_secure_storage + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + url: "https://pub.dev" + source: hosted + version: "10.0.0" flutter_secure_storage_darwin: - dependency: "direct overridden" + dependency: transitive description: - path: flutter_secure_storage_darwin - ref: a06ead81809c900e7fc421a30db0adf3b5919139 - resolved-ref: a06ead81809c900e7fc421a30db0adf3b5919139 - url: "https://github.com/juliansteenbakker/flutter_secure_storage.git" - source: git - version: "0.1.0" + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "9b4b73127e857cd3117d43a70fa3dddadb6e0b253be62e6a6ab85caa0742182c" + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -777,18 +775,25 @@ packages: dependency: transitive description: name: flutter_secure_storage_web - sha256: "4c3f233e739545c6cb09286eeec1cc4744138372b985113acc904f7263bef517" + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: ff32af20f70a8d0e59b2938fc92de35b54a74671041c814275afd80e27df9f21 + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.0" + flutter_sharing_intent: + dependency: "direct main" + description: + path: "dependencies/flutter_sharing_intent" + relative: true + source: path + version: "2.0.4" flutter_svg: dependency: "direct main" description: @@ -1243,11 +1248,10 @@ packages: no_screenshot: dependency: "direct main" description: - name: no_screenshot - sha256: ec3d86d7ee89a09c3a3939c1003012536ba4b3fcb4f8cbd23d87ada595c99258 - url: "https://pub.dev" - source: hosted - version: "0.3.1" + path: "dependencies/no_screenshot" + relative: true + source: path + version: "0.3.2-beta.3" objective_c: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8b16d26..d441e98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: url_launcher: ^6.3.2 vector_graphics: ^1.1.19 video_player: ^2.10.1 + in_app_purchase: ^3.2.3 # Trusted publisher fluttercommunity.dev @@ -54,44 +55,50 @@ dependencies: scrollable_positioned_list: ^0.3.8 # google.dev + # Flutter Favorite + provider: ^6.1.2 + drift: ^2.25.1 + drift_flutter: ^0.2.4 + flutter_local_notifications: ^19.1.0 + sentry_flutter: ^9.8.0 + + + # With high download + app_links: ^7.0.0 # 1.6 mio + image: ^4.3.0 # 3.3 mio + archive: ^4.0.7 # 6.5 mio + file_picker: ^10.3.6 # 2 mio + get: ^4.7.2 # 740 k + flutter_secure_storage: ^10.0.0 # 1.85 mio + permission_handler: ^12.0.0+1 # 2 mio + + # Not yet checked - archive: ^4.0.7 audio_waveforms: ^2.0.0 avatar_maker: ^0.4.0 background_downloader: ^9.4.0 cached_network_image: ^3.4.1 cryptography_flutter_plus: ^2.3.4 cryptography_plus: ^2.7.0 - drift: ^2.25.1 - drift_flutter: ^0.2.4 ffmpeg_kit_flutter_new: ^4.1.0 - file_picker: ^10.3.6 flutter_android_volume_keydown: ^1.0.1 flutter_image_compress: ^2.4.0 - flutter_local_notifications: ^19.1.0 - flutter_secure_storage: - git: - url: https://github.com/juliansteenbakker/flutter_secure_storage.git - ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop - path: flutter_secure_storage/ flutter_volume_controller: ^1.3.4 gal: ^2.3.1 - get: ^4.7.2 google_mlkit_barcode_scanning: ^0.14.1 - image: ^4.3.0 - no_screenshot: ^0.3.1 - permission_handler: ^12.0.0+1 - provider: ^6.1.2 - restart_app: ^1.3.2 - sentry_flutter: ^9.8.0 - app_links: ^7.0.0 - in_app_purchase: ^3.2.3 + + # flutter_secure_storage: + # git: + # url: https://github.com/juliansteenbakker/flutter_secure_storage.git + # ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop + # path: flutter_secure_storage/ # Overwritten by self-controlled repository emoji_picker_flutter: ^4.3.0 # Packages which got overwritten using the twonly-app-dependencies repository + restart_app: ^1.3.2 photo_view: ^0.15.0 hashlib: ^2.0.0 libsignal_protocol_dart: ^0.7.4 @@ -100,6 +107,8 @@ dependencies: introduction_screen: ^4.0.0 qr_flutter: ^4.1.0 hand_signature: ^3.0.3 + flutter_sharing_intent: ^2.0.4 + no_screenshot: ^0.3.1 dependency_overrides: dots_indicator: @@ -110,6 +119,8 @@ dependency_overrides: 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: @@ -134,6 +145,8 @@ dependency_overrides: path: ./dependencies/x25519 qr_flutter: path: ./dependencies/qr_flutter + no_screenshot: + path: ./dependencies/no_screenshot camera_android_camerax: # path: ../flutter-packages/packages/camera/camera_android_camerax git: @@ -149,11 +162,11 @@ dependency_overrides: git: url: https://github.com/yenchieh/flutter_android_volume_keydown.git branch: fix/lStar-not-found-error - flutter_secure_storage_darwin: - git: - url: https://github.com/juliansteenbakker/flutter_secure_storage.git - ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop - path: flutter_secure_storage_darwin/ + # flutter_secure_storage_darwin: + # git: + # url: https://github.com/juliansteenbakker/flutter_secure_storage.git + # ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop + # path: flutter_secure_storage_darwin/ # hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY dev_dependencies: From 57c73a86ace92276f4e7f137d03414a85b70aac1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 27 Dec 2025 16:24:31 +0100 Subject: [PATCH 13/14] fix #305 --- lib/src/localization/app_de.arb | 4 +- lib/src/localization/app_en.arb | 4 +- .../generated/app_localizations.dart | 12 ++ .../generated/app_localizations_de.dart | 8 ++ .../generated/app_localizations_en.dart | 8 ++ lib/src/views/camera/share_image_view.dart | 4 +- lib/src/views/memories/memories.view.dart | 113 +++++++++++++++--- 7 files changed, 134 insertions(+), 19 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 1361dac..57d87a7 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -459,5 +459,7 @@ "linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!", "startWithCameraOpen": "Mit geöffneter Kamera starten", "showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen", - "verifiedPublicKey": "Der öffentliche Schlüssel von {username} wurde überprüft und ist gültig." + "verifiedPublicKey": "Der öffentliche Schlüssel von {username} wurde überprüft und ist gültig.", + "memoriesAYearAgo": "Vor einem Jahr", + "memoriesXYearsAgo": "Vor {years} Jahren" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 96ce280..ddb53ec 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -489,5 +489,7 @@ "linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!", "startWithCameraOpen": "Start with camera open", "showImagePreviewWhenSending": "Display image preview when selecting recipients", - "verifiedPublicKey": "The public key of {username} has been verified and is valid." + "verifiedPublicKey": "The public key of {username} has been verified and is valid.", + "memoriesAYearAgo": "One year ago", + "memoriesXYearsAgo": "{years} years ago" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index d337892..d2acbb5 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2857,6 +2857,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'The public key of {username} has been verified and is valid.'** String verifiedPublicKey(Object username); + + /// No description provided for @memoriesAYearAgo. + /// + /// In en, this message translates to: + /// **'One year ago'** + String get memoriesAYearAgo; + + /// No description provided for @memoriesXYearsAgo. + /// + /// In en, this message translates to: + /// **'{years} years ago'** + String memoriesXYearsAgo(Object years); } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 61bae51..e15da68 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1582,4 +1582,12 @@ class AppLocalizationsDe extends AppLocalizations { String verifiedPublicKey(Object username) { return 'Der öffentliche Schlüssel von $username wurde überprüft und ist gültig.'; } + + @override + String get memoriesAYearAgo => 'Vor einem Jahr'; + + @override + String memoriesXYearsAgo(Object years) { + return 'Vor $years Jahren'; + } } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 04d56d9..6d119c3 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1572,4 +1572,12 @@ class AppLocalizationsEn extends AppLocalizations { String verifiedPublicKey(Object username) { return 'The public key of $username has been verified and is valid.'; } + + @override + String get memoriesAYearAgo => 'One year ago'; + + @override + String memoriesXYearsAgo(Object years) { + return '$years years ago'; + } } diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 6ee9eed..db23ac3 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -254,10 +254,10 @@ class _ShareImageView extends State { border: Border.all(color: context.color.primary, width: 3), color: context.color.primary, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(12), ), child: ClipRRect( - borderRadius: BorderRadius.circular(7), + borderRadius: BorderRadius.circular(12), child: Image.memory(_imageBytes!), ), ), diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 3d48f82..8125f4a 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -18,12 +18,13 @@ class MemoriesView extends StatefulWidget { } class MemoriesViewState extends State { - bool verticalGallery = false; List galleryItems = []; Map> orderedByMonth = {}; List months = []; StreamSubscription>? messageSub; + final Map> _galleryItemsLastYears = {}; + @override void initState() { super.initState(); @@ -46,6 +47,9 @@ class MemoriesViewState extends State { months = []; var lastMonth = ''; galleryItems = []; + + final now = DateTime.now(); + for (final mediaFile in mediaFiles) { final mediaService = MediaFileService(mediaFile); if (!mediaService.imagePreviewAvailable) continue; @@ -54,12 +58,21 @@ class MemoriesViewState extends State { await mediaService.createThumbnail(); } } - galleryItems.add( - MemoryItem( - mediaService: mediaService, - messages: [], - ), + final item = MemoryItem( + mediaService: mediaService, + messages: [], ); + galleryItems.add(item); + if (mediaFile.createdAt.month == now.month && + mediaFile.createdAt.day == now.day) { + final diff = now.year - mediaFile.createdAt.year; + if (diff > 0) { + if (!_galleryItemsLastYears.containsKey(diff)) { + _galleryItemsLastYears[diff] = []; + } + _galleryItemsLastYears[diff]!.add(item); + } + } } galleryItems.sort( (a, b) => b.mediaService.mediaFile.createdAt.compareTo( @@ -94,8 +107,83 @@ class MemoriesViewState extends State { ), ) : ListView.builder( - itemCount: months.length * 2, + itemCount: (months.length * 2) + + (_galleryItemsLastYears.isEmpty ? 0 : 1), itemBuilder: (context, mIndex) { + if (_galleryItemsLastYears.isNotEmpty && mIndex == 0) { + return SizedBox( + height: 140, + width: MediaQuery.sizeOf(context).width, + child: ListView( + scrollDirection: Axis.horizontal, + children: _galleryItemsLastYears.entries.map( + (item) { + var text = context.lang.memoriesAYearAgo; + if (item.key > 1) { + text = context.lang.memoriesXYearsAgo(item.key); + } + return GestureDetector( + onTap: () async { + await open(context, item.value, 0); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + spreadRadius: -12, + blurRadius: 12, + ), + ], + ), + clipBehavior: Clip.hardEdge, + height: 150, + width: 120, + child: Stack( + children: [ + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + item.value.first.mediaService + .storedPath, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + bottom: 10, + left: 0, + right: 0, + child: Text( + text, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + shadows: [ + Shadow( + color: + Color.fromARGB(122, 0, 0, 0), + blurRadius: 5, + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ).toList(), + ), + ); + } + if (_galleryItemsLastYears.isNotEmpty) { + mIndex -= 1; + } if (mIndex.isEven) { return Padding( padding: const EdgeInsets.all(8), @@ -117,7 +205,7 @@ class MemoriesViewState extends State { return MemoriesItemThumbnail( galleryItem: galleryItems[gaIndex], onTap: () async { - await open(context, gaIndex); + await open(context, galleryItems, gaIndex); }, ); }, @@ -128,7 +216,8 @@ class MemoriesViewState extends State { ); } - Future open(BuildContext context, int index) async { + Future open( + BuildContext context, List galleryItems, int index) async { await Navigator.push( context, PageRouteBuilder( @@ -136,13 +225,7 @@ class MemoriesViewState extends State { pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( galleryItems: galleryItems, initialIndex: index, - scrollDirection: verticalGallery ? Axis.vertical : Axis.horizontal, ), - // transitionsBuilder: (context, animation, secondaryAnimation, child) { - // return child; - // }, - // transitionDuration: Duration.zero, - // reverseTransitionDuration: Duration.zero, ), ) as bool?; if (mounted) setState(() {}); From 20a2d6175115d969bf6840b5730d588faaad4b2f Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 27 Dec 2025 21:31:53 +0100 Subject: [PATCH 14/14] bump version --- CHANGELOG.md | 8 ++++++++ lib/src/services/api.service.dart | 3 ++- lib/src/services/intent/links.intent.dart | 2 +- lib/src/views/camera/share_image_view.dart | 12 ++++++------ lib/src/views/memories/memories.view.dart | 5 ++++- pubspec.yaml | 2 +- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c538d..0293ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 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 + ## 0.0.74 - Improving uploading speed diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 223ea00..850cc6a 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -50,7 +50,7 @@ final lockRetransStore = Mutex(); /// errors or network changes. class ApiService { ApiService(); - final String apiHost = kReleaseMode ? 'api.twonly.eu' : '192.168.178.88:3030'; + final String apiHost = kReleaseMode ? 'api.twonly.eu' : '192.168.2.178:3030'; // final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; final String apiSecure = kReleaseMode ? 's' : ''; @@ -182,6 +182,7 @@ class ApiService { Future _onDone() async { Log.info('websocket closed without error'); + _reconnectionDelay = 60 * 2; // the server closed the connection... await onClosed(); } diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index f951695..c4a1066 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -163,7 +163,7 @@ Future handleIntentSharedFile( switch (file.type) { case SharedMediaType.URL: - await handleIntentUrl(context, Uri.parse(file.value!)); + // await handleIntentUrl(context, Uri.parse(file.value!)); case SharedMediaType.IMAGE: var type = MediaType.image; if (file.value!.endsWith('.gif')) { diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index db23ac3..83a4ed3 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -237,9 +237,9 @@ class _ShareImageView extends State { ), ), floatingActionButton: SizedBox( - height: 148, + height: 168, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.only(bottom: 20, right: 20), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -252,7 +252,7 @@ class _ShareImageView extends State { clipBehavior: Clip.hardEdge, decoration: BoxDecoration( border: - Border.all(color: context.color.primary, width: 3), + Border.all(color: context.color.primary, width: 2), color: context.color.primary, borderRadius: BorderRadius.circular(12), ), @@ -304,9 +304,9 @@ class _ShareImageView extends State { const EdgeInsets.symmetric(vertical: 10, horizontal: 30), ), backgroundColor: WidgetStateProperty.all( - mediaStoreFutureReady || widget.selectedGroupIds.isEmpty - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, + !mediaStoreFutureReady || widget.selectedGroupIds.isEmpty + ? context.color.onSurface + : context.color.primary, ), ), label: Text( diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 8125f4a..483053d 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -217,7 +217,10 @@ class MemoriesViewState extends State { } Future open( - BuildContext context, List galleryItems, int index) async { + BuildContext context, + List galleryItems, + int index, + ) async { await Navigator.push( context, PageRouteBuilder( diff --git a/pubspec.yaml b/pubspec.yaml index d441e98..932ff83 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.0.79+79 +version: 0.0.80+80 environment: sdk: ^3.6.0