mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
Merge pull request #358 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- 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
This commit is contained in:
commit
41dfd54e81
50 changed files with 2010 additions and 436 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@
|
|||
<data android:scheme="http" android:host="me.twonly.eu" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
|
@ -46,19 +56,11 @@
|
|||
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
|
||||
tools:node="remove">
|
||||
</service>
|
||||
|
||||
<!-- <service
|
||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:foregroundServiceType="dataSync|remoteMessaging"
|
||||
android:exported="false" /> -->
|
||||
|
||||
<meta-data
|
||||
android:name="eu.twonly.service.TWONLY_LOGO"
|
||||
android:resource="@drawable/ic_launcher_foreground" />
|
||||
</application>
|
||||
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit fb66274bf729cde6f7184ec6f7f9ea89f12450fd
|
||||
Subproject commit 7930d9727019344238297d810661bc3e8f724c37
|
||||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
|
@ -87,11 +99,15 @@
|
|||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
|
|
@ -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 = "<group>";
|
||||
};
|
||||
D25D4D712EFF41DB0029F805 /* ShareExtension */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
D25D4D7E2EFF41DB0029F805 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */,
|
||||
);
|
||||
path = ShareExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -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 */;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,21 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<!--Disable Firebase Telemetry-->
|
||||
<key>firebase_performance_collection_deactivated</key>
|
||||
<true/>
|
||||
<!--...-->
|
||||
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
20
ios/Runner/RunnerDebug.entitlements
Normal file
20
ios/Runner/RunnerDebug.entitlements
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:me.twonly.eu</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.eu.twonly.shareIntent</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)eu.twonly.shared</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
24
ios/ShareExtension/Base.lproj/MainInterface.storyboard
Normal file
24
ios/ShareExtension/Base.lproj/MainInterface.storyboard
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Share View Controller-->
|
||||
<scene sceneID="ceB-am-kn3">
|
||||
<objects>
|
||||
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
553
ios/ShareExtension/FSIShareViewController.swift
Normal file
553
ios/ShareExtension/FSIShareViewController.swift
Normal file
|
|
@ -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[..<idx])
|
||||
} else {
|
||||
hostAppBundleIdentifier = shareExtId
|
||||
}
|
||||
let custom = Bundle.main.object(forInfoDictionaryKey: kAppGroupIdKey) as? String
|
||||
appGroupId = custom ?? "group.\(hostAppBundleIdentifier)"
|
||||
log("loaded host=\(hostAppBundleIdentifier) group=\(appGroupId)")
|
||||
}
|
||||
|
||||
// MARK: - Attachment processing (clean RSI style, preserve FSI features)
|
||||
private func processAttachments() {
|
||||
guard let content = extensionContext?.inputItems.first as? NSExtensionItem else {
|
||||
completeAndExit()
|
||||
return
|
||||
}
|
||||
|
||||
guard let attachments = content.attachments, !attachments.isEmpty else {
|
||||
completeAndExit()
|
||||
return
|
||||
}
|
||||
|
||||
// Use DispatchGroup to wait for async loads
|
||||
let group = DispatchGroup()
|
||||
for (index, provider) in attachments.enumerated() {
|
||||
group.enter()
|
||||
// Try all SharedMediaType options similar to RSI but preserve explicit FSI order
|
||||
if provider.isImage {
|
||||
provider.loadItem(forTypeIdentifier: UType.image, options: nil) { [weak self] data, error in
|
||||
defer { group.leave() }
|
||||
guard let self = self, error == nil else { self?.dismissWithError(); return }
|
||||
self.handleImageItem(data: data, index: index, total: attachments.count)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if provider.isMovie {
|
||||
provider.loadItem(forTypeIdentifier: UType.movie, options: nil) { [weak self] data, error in
|
||||
defer { group.leave() }
|
||||
guard let self = self, error == nil else { self?.dismissWithError(); return }
|
||||
self.handleVideoItem(data: data, index: index, total: attachments.count)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if provider.isFile {
|
||||
provider.loadItem(forTypeIdentifier: UType.fileURL, options: nil) { [weak self] data, error in
|
||||
defer { group.leave() }
|
||||
guard let self = self, error == nil else { self?.dismissWithError(); return }
|
||||
self.handleFileItem(data: data, index: index, total: attachments.count)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if provider.isURL {
|
||||
provider.loadItem(forTypeIdentifier: UType.url, options: nil) { [weak self] data, error in
|
||||
defer { group.leave() }
|
||||
guard let self = self, error == nil else { self?.dismissWithError(); return }
|
||||
self.handleUrlItem(data: data, index: index, total: attachments.count)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if provider.isText {
|
||||
let id = provider.hasItemConformingToTypeIdentifier(UType.plainText)
|
||||
? UType.plainText
|
||||
: UType.text
|
||||
provider.loadItem(forTypeIdentifier: id, options: nil) { [weak self] data, error in
|
||||
defer { group.leave() }
|
||||
guard let self = self, error == nil else { self?.dismissWithError(); return }
|
||||
self.handleTextItem(data: data, index: index, total: attachments.count)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if provider.isData {
|
||||
provider.loadItem(forTypeIdentifier: UType.data, options: nil) { [weak self] data, error in
|
||||
defer { group.leave() }
|
||||
guard let self = self, error == nil else { self?.dismissWithError(); return }
|
||||
self.handleFileItem(data: data, index: index, total: attachments.count)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if provider.isItem {
|
||||
provider.loadItem(forTypeIdentifier: UType.item, options: nil) { [weak self] data, error in
|
||||
defer { group.leave() }
|
||||
guard let self = self, error == nil else { self?.dismissWithError(); return }
|
||||
self.handleFileItem(data: data, index: index, total: attachments.count)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
log("Unknown provider type: \(provider.registeredTypeIdentifiers)")
|
||||
|
||||
// Unknown type: just leave
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// if we have media -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
35
ios/ShareExtension/Info.plist
Normal file
35
ios/ShareExtension/Info.plist
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>PHSupportedMediaTypes</key>
|
||||
<array>
|
||||
<string>Video</string>
|
||||
<string>Image</string>
|
||||
</array>
|
||||
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
10
ios/ShareExtension/ShareExtensionDebug.entitlements
Normal file
10
ios/ShareExtension/ShareExtensionDebug.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.eu.twonly.shareIntent</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
3
ios/ShareExtension/ShareViewController.swift
Normal file
3
ios/ShareExtension/ShareViewController.swift
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class ShareViewController: FSIShareViewController {
|
||||
|
||||
}
|
||||
11
lib/app.dart
11
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';
|
||||
|
|
@ -93,6 +94,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
listenable: context.watch<SettingsChangeProvider>(),
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return MaterialApp(
|
||||
scaffoldMessengerKey: globalRootScaffoldMessengerKey,
|
||||
restorationScopeId: 'app',
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
|
|
@ -157,11 +159,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
bool _showOnboarding = true;
|
||||
bool _isLoaded = false;
|
||||
bool _skipBackup = false;
|
||||
int _initialPage = 0;
|
||||
|
||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_initialPage = widget.initialPage;
|
||||
initAsync();
|
||||
super.initState();
|
||||
}
|
||||
|
|
@ -173,6 +177,9 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
if (gUser.appVersion < 62) {
|
||||
_showDatabaseMigration = true;
|
||||
}
|
||||
if (!gUser.startWithCameraOpen) {
|
||||
_initialPage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isUserCreated && !_showDatabaseMigration) {
|
||||
|
|
@ -205,7 +212,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
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 +221,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
);
|
||||
} else {
|
||||
child = HomeView(
|
||||
initialPage: widget.initialPage,
|
||||
initialPage: _initialPage,
|
||||
);
|
||||
}
|
||||
} else if (_showOnboarding) {
|
||||
|
|
|
|||
|
|
@ -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<ScaffoldMessengerState> globalRootScaffoldMessengerKey =
|
||||
GlobalKey<ScaffoldMessengerState>();
|
||||
|
|
|
|||
|
|
@ -7111,10 +7111,7 @@ class $GroupHistoriesTable extends GroupHistories
|
|||
@override
|
||||
late final GeneratedColumn<int> affectedContactId = GeneratedColumn<int>(
|
||||
'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<GroupHistory>>
|
||||
_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<int>('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<bool> groupHistoriesRefs(
|
||||
Expression<bool> 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<T> groupHistoriesRefs<T extends Object>(
|
||||
Expression<T> 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<Contact, $ContactsTable,
|
||||
GroupHistory>(
|
||||
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<bool> 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<int>('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<int> get affectedContactId => $composableBuilder(
|
||||
column: $table.affectedContactId,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> 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<int> get affectedContactId => $composableBuilder(
|
||||
column: $table.affectedContactId,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> 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<String> get groupHistoryId => $composableBuilder(
|
||||
column: $table.groupHistoryId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get affectedContactId => $composableBuilder(
|
||||
column: $table.affectedContactId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> 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;
|
||||
|
|
|
|||
|
|
@ -456,5 +456,10 @@
|
|||
"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",
|
||||
"showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen",
|
||||
"verifiedPublicKey": "Der öffentliche Schlüssel von {username} wurde überprüft und ist gültig.",
|
||||
"memoriesAYearAgo": "Vor einem Jahr",
|
||||
"memoriesXYearsAgo": "Vor {years} Jahren"
|
||||
}
|
||||
|
|
@ -486,5 +486,10 @@
|
|||
"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",
|
||||
"showImagePreviewWhenSending": "Display image preview when selecting recipients",
|
||||
"verifiedPublicKey": "The public key of {username} has been verified and is valid.",
|
||||
"memoriesAYearAgo": "One year ago",
|
||||
"memoriesXYearsAgo": "{years} years ago"
|
||||
}
|
||||
|
|
@ -2839,6 +2839,36 @@ 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;
|
||||
|
||||
/// No description provided for @showImagePreviewWhenSending.
|
||||
///
|
||||
/// 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);
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -1570,4 +1570,24 @@ 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';
|
||||
|
||||
@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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get memoriesAYearAgo => 'Vor einem Jahr';
|
||||
|
||||
@override
|
||||
String memoriesXYearsAgo(Object years) {
|
||||
return 'Vor $years Jahren';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1560,4 +1560,24 @@ 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';
|
||||
|
||||
@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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get memoriesAYearAgo => 'One year ago';
|
||||
|
||||
@override
|
||||
String memoriesXYearsAgo(Object years) {
|
||||
return '$years years ago';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ class UserData {
|
|||
@JsonKey(defaultValue: true)
|
||||
bool showFeedbackShortcut = true;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool showShowImagePreviewWhenSending = true;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool startWithCameraOpen = true;
|
||||
|
||||
List<String>? preSelectedEmojies;
|
||||
|
||||
Map<String, List<String>>? autoDownloadOptions;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList()
|
||||
|
|
@ -61,7 +64,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..lastChangeLogHash = (json['lastChangeLogHash'] as List<dynamic>?)
|
||||
?.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 +96,9 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'defaultShowTime': instance.defaultShowTime,
|
||||
'requestedAudioPermission': instance.requestedAudioPermission,
|
||||
'showFeedbackShortcut': instance.showFeedbackShortcut,
|
||||
'showShowImagePreviewWhenSending':
|
||||
instance.showShowImagePreviewWhenSending,
|
||||
'startWithCameraOpen': instance.startWithCameraOpen,
|
||||
'preSelectedEmojies': instance.preSelectedEmojies,
|
||||
'autoDownloadOptions': instance.autoDownloadOptions,
|
||||
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
|
||||
|
|
|
|||
|
|
@ -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.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<void> _onDone() async {
|
||||
Log.info('websocket closed without error');
|
||||
_reconnectionDelay = 60 * 2; // the server closed the connection...
|
||||
await onClosed();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ Future<MediaFileService?> initializeMediaUpload(
|
|||
Future<void> insertMediaFileInMessagesTable(
|
||||
MediaFileService mediaService,
|
||||
List<String> groupIds,
|
||||
Future<Uint8List?>? imageStoreAwait,
|
||||
) async {
|
||||
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
||||
const MediaFilesCompanion(
|
||||
|
|
@ -117,6 +118,13 @@ Future<void> insertMediaFileInMessagesTable(
|
|||
}
|
||||
}
|
||||
|
||||
if (imageStoreAwait != null) {
|
||||
if (await imageStoreAwait == null) {
|
||||
Log.error('image store as original did return false...');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(startBackgroundMediaUpload(mediaService));
|
||||
}
|
||||
|
||||
|
|
|
|||
180
lib/src/services/intent/links.intent.dart
Normal file
180
lib/src/services/intent/links.intent.dart
Normal file
|
|
@ -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<void> 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<void> handleIntentMediaFile(
|
||||
BuildContext context,
|
||||
String filePath,
|
||||
MediaType type,
|
||||
) async {
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
Log.error('The shared intent file does not exits.');
|
||||
return;
|
||||
}
|
||||
|
||||
final newMediaService = await initializeMediaUpload(
|
||||
type,
|
||||
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<void> handleIntentSharedFile(
|
||||
BuildContext context,
|
||||
List<SharedFile> 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...
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
104
lib/src/utils/screenshot.dart
Normal file
104
lib/src/utils/screenshot.dart
Normal file
|
|
@ -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<Uint8List>? imageBytesFuture;
|
||||
File? file;
|
||||
|
||||
Future<Uint8List?> 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<ScreenshotImage?> 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<Screenshot> createState() {
|
||||
return ScreenshotState();
|
||||
}
|
||||
}
|
||||
|
||||
class ScreenshotState extends State<Screenshot> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -316,7 +317,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
|
||||
Future<void> takePicture() async {
|
||||
if (_sharePreviewIsShown || _isVideoRecording) return;
|
||||
late Future<Uint8List?> imageBytes;
|
||||
|
||||
setState(() {
|
||||
_sharePreviewIsShown = true;
|
||||
|
|
@ -337,18 +337,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
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;
|
||||
}
|
||||
|
||||
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 +357,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
|
||||
Future<bool> pushMediaEditor(
|
||||
Future<Uint8List?>? imageBytes,
|
||||
ScreenshotImage? imageBytes,
|
||||
File? videoFilePath, {
|
||||
bool sharedFromGallery = false,
|
||||
MediaType? mediaType,
|
||||
|
|
@ -397,6 +397,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
sharedFromGallery: sharedFromGallery,
|
||||
sendToGroup: widget.sendToGroup,
|
||||
mediaFileService: mediaFileService,
|
||||
mainCameraController: mc,
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return child;
|
||||
|
|
@ -477,7 +478,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
Log.info('Picket from gallery: ${pickedFile.path}');
|
||||
|
||||
File? videoFilePath;
|
||||
Future<Uint8List>? imageBytes;
|
||||
ScreenshotImage? image;
|
||||
MediaType? mediaType;
|
||||
|
||||
final isImage =
|
||||
|
|
@ -486,13 +487,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
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,
|
||||
|
|
@ -639,10 +640,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ 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:screenshot/screenshot.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';
|
||||
import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart';
|
||||
|
||||
|
|
@ -219,6 +221,20 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -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<bool> Function() storeImageAsOriginal;
|
||||
final Future<Uint8List?> Function() storeImageAsOriginal;
|
||||
final bool displayButtonLabel;
|
||||
final MediaFileService mediaService;
|
||||
final bool isLoading;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,14 @@ class LayersViewer extends StatelessWidget {
|
|||
children: [
|
||||
...layers.whereType<BackgroundLayerData>().map((layerItem) {
|
||||
return BackgroundLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
}),
|
||||
...layers.whereType<FilterLayerData>().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<TextLayerData>().map((layerItem) {
|
||||
return TextLayer(
|
||||
// key: GlobalKey(),
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class EmojiPickerBottom extends StatelessWidget {
|
|||
Navigator.pop(
|
||||
context,
|
||||
EmojiLayerData(
|
||||
key: GlobalKey(),
|
||||
text: emoji.emoji,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
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';
|
||||
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,7 +15,9 @@ 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';
|
||||
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
|
||||
|
|
@ -38,11 +40,13 @@ class ShareImageEditorView extends StatefulWidget {
|
|||
super.key,
|
||||
this.imageBytesFuture,
|
||||
this.sendToGroup,
|
||||
this.mainCameraController,
|
||||
});
|
||||
final Future<Uint8List?>? imageBytesFuture;
|
||||
final ScreenshotImage? imageBytesFuture;
|
||||
final Group? sendToGroup;
|
||||
final bool sharedFromGallery;
|
||||
final MediaFileService mediaFileService;
|
||||
final MainCameraController? mainCameraController;
|
||||
@override
|
||||
State<ShareImageEditorView> createState() => _ShareImageEditorView();
|
||||
}
|
||||
|
|
@ -69,7 +73,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
super.initState();
|
||||
|
||||
if (media.type != MediaType.gif) {
|
||||
layers.add(FilterLayerData());
|
||||
layers.add(FilterLayerData(key: GlobalKey()));
|
||||
}
|
||||
|
||||
if (widget.sendToGroup != null) {
|
||||
|
|
@ -82,9 +86,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,6 +135,82 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _setMaxShowTime(int? maxShowTime) async {
|
||||
await mediaService.setDisplayLimit(maxShowTime);
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
await updateUserdata((user) {
|
||||
user.defaultShowTime = maxShowTime;
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _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<void>(
|
||||
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<Widget> get actionsAtTheRight {
|
||||
if (layers.isNotEmpty &&
|
||||
layers.last.isEditing &&
|
||||
|
|
@ -147,6 +229,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
removedLayers.clear();
|
||||
layers.add(
|
||||
TextLayerData(
|
||||
key: GlobalKey(),
|
||||
textLayersBefore: layers.whereType<TextLayerData>().length,
|
||||
),
|
||||
);
|
||||
|
|
@ -161,7 +244,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
onPressed: () async {
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
layers.add(DrawLayerData());
|
||||
layers.add(DrawLayerData(key: GlobalKey()));
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
|
|
@ -199,33 +282,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
: 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)
|
||||
|
|
@ -380,11 +437,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> getEditedImageBytes() async {
|
||||
Future<ScreenshotImage?> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -409,22 +466,33 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
return image;
|
||||
}
|
||||
|
||||
Future<bool> storeImageAsOriginal() async {
|
||||
Future<Uint8List?> storeImageAsOriginal() async {
|
||||
if (mediaService.overlayImagePath.existsSync()) {
|
||||
mediaService.overlayImagePath.deleteSync();
|
||||
}
|
||||
if (mediaService.tempPath.existsSync()) {
|
||||
mediaService.tempPath.deleteSync();
|
||||
}
|
||||
if (mediaService.originalPath.existsSync()) {
|
||||
if (media.type != MediaType.video) {
|
||||
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}');
|
||||
}
|
||||
|
|
@ -444,12 +512,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
.renameSync(MediaFileService(mediaFile).storedPath.path);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
|
||||
imageBytes = await imageBytesFuture;
|
||||
|
||||
Future<void> 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());
|
||||
|
||||
|
|
@ -458,9 +525,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
|
@ -476,18 +550,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
sendingOrLoadingImage = true;
|
||||
});
|
||||
|
||||
await storeImageAsOriginal();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Insert media file into the messages database and start uploading process in the background
|
||||
await insertMediaFileInMessagesTable(
|
||||
unawaited(
|
||||
insertMediaFileInMessagesTable(
|
||||
mediaService,
|
||||
[widget.sendToGroup!.groupId],
|
||||
storeImageAsOriginal(),
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -526,6 +600,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
removedLayers.clear();
|
||||
layers.add(
|
||||
TextLayerData(
|
||||
key: GlobalKey(),
|
||||
offset: Offset(0, tabDownPosition),
|
||||
textLayersBefore: layers.whereType<TextLayerData>().length,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<String> selectedGroupIds;
|
||||
final void Function(String, bool) updateSelectedGroupIds;
|
||||
final Future<bool>? mediaStoreFuture;
|
||||
final Future<Uint8List?>? mediaStoreFuture;
|
||||
final MediaFileService mediaFileService;
|
||||
|
||||
@override
|
||||
|
|
@ -41,6 +43,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
|
||||
bool sendingImage = false;
|
||||
bool mediaStoreFutureReady = false;
|
||||
Uint8List? _imageBytes;
|
||||
bool hideArchivedUsers = true;
|
||||
final TextEditingController searchUserName = TextEditingController();
|
||||
late StreamSubscription<List<Group>> allGroupSub;
|
||||
|
|
@ -63,10 +66,9 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
|
||||
Future<void> 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<ShareImageView> {
|
|||
),
|
||||
),
|
||||
floatingActionButton: SizedBox(
|
||||
height: 120,
|
||||
height: 168,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
padding: const EdgeInsets.only(bottom: 20, right: 20),
|
||||
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: 2),
|
||||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.memory(_imageBytes!),
|
||||
),
|
||||
),
|
||||
),
|
||||
FilledButton.icon(
|
||||
icon: !mediaStoreFutureReady || sendingImage
|
||||
? SizedBox(
|
||||
|
|
@ -265,6 +286,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
await insertMediaFileInMessagesTable(
|
||||
widget.mediaFileService,
|
||||
widget.selectedGroupIds.toList(),
|
||||
null,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
|
|
@ -282,13 +304,13 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
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(
|
||||
context.lang.shareImagedEditorSendImage,
|
||||
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,22 +373,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
);
|
||||
} 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,
|
||||
return BlinkWidget(
|
||||
enabled: focusedScrollItem == i,
|
||||
child: ChatListEntry(
|
||||
key: Key(chatMessage.messageId),
|
||||
message: messages[i].message!,
|
||||
nextMessage:
|
||||
(i > 0) ? messages[i - 1].message : null,
|
||||
nextMessage: (i > 0) ? messages[i - 1].message : null,
|
||||
prevMessage: ((i + 1) < messages.length)
|
||||
? messages[i + 1].message
|
||||
: null,
|
||||
|
|
@ -402,7 +393,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
textFieldFocus.requestFocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||
await insertMediaFileInMessagesTable(
|
||||
mediaFileService,
|
||||
[widget.group.groupId],
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,8 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
);
|
||||
};
|
||||
}
|
||||
if (mediaFile.uploadState == UploadState.preprocessing) {
|
||||
if (mediaFile.uploadState == UploadState.preprocessing ||
|
||||
mediaFile.uploadState == UploadState.initialized) {
|
||||
text = context.lang.inProcess;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
lib/src/views/components/blink.component.dart
Normal file
75
lib/src/views/components/blink.component.dart
Normal file
|
|
@ -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<BlinkWidget> createState() => _BlinkWidgetState();
|
||||
}
|
||||
|
||||
class _BlinkWidgetState extends State<BlinkWidget>
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HomeView> {
|
|||
final MainCameraController _mainCameraController = MainCameraController();
|
||||
|
||||
final PageController homeViewPageController = PageController(initialPage: 1);
|
||||
late StreamSubscription<List<SharedFile>> _intentStreamSub;
|
||||
late StreamSubscription<Uri> _deepLinkSub;
|
||||
|
||||
double buttonDiameter = 100;
|
||||
|
|
@ -121,99 +115,21 @@ class HomeViewState extends State<HomeView> {
|
|||
|
||||
// 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];
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (username == gUser.username) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const PublicProfileView();
|
||||
_intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen(
|
||||
(f) {
|
||||
if (mounted) handleIntentSharedFile(context, f);
|
||||
},
|
||||
// ignore: inference_failure_on_untyped_parameter
|
||||
onError: (err) {
|
||||
Log.error('getIntentDataStream error: $err');
|
||||
},
|
||||
),
|
||||
);
|
||||
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<HomeView> {
|
|||
unawaited(selectNotificationStream.close());
|
||||
disableCameraTimer?.cancel();
|
||||
_mainCameraController.closeCamera();
|
||||
_intentStreamSub.cancel();
|
||||
_deepLinkSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -230,11 +147,11 @@ class HomeViewState extends State<HomeView> {
|
|||
final notificationAppLaunchDetails =
|
||||
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||
|
||||
if (notificationAppLaunchDetails != null) {
|
||||
if (notificationAppLaunchDetails.didNotificationLaunchApp) {
|
||||
if (widget.initialPage == 0 ||
|
||||
(notificationAppLaunchDetails != null &&
|
||||
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
||||
globalUpdateOfHomeViewPageIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();
|
||||
if (draftMedia != null) {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ class MemoriesView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class MemoriesViewState extends State<MemoriesView> {
|
||||
bool verticalGallery = false;
|
||||
List<MemoryItem> galleryItems = [];
|
||||
Map<String, List<int>> orderedByMonth = {};
|
||||
List<String> months = [];
|
||||
StreamSubscription<List<MediaFile>>? messageSub;
|
||||
|
||||
final Map<int, List<MemoryItem>> _galleryItemsLastYears = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -46,6 +47,9 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
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<MemoriesView> {
|
|||
await mediaService.createThumbnail();
|
||||
}
|
||||
}
|
||||
galleryItems.add(
|
||||
MemoryItem(
|
||||
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<MemoriesView> {
|
|||
),
|
||||
)
|
||||
: 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<MemoriesView> {
|
|||
return MemoriesItemThumbnail(
|
||||
galleryItem: galleryItems[gaIndex],
|
||||
onTap: () async {
|
||||
await open(context, gaIndex);
|
||||
await open(context, galleryItems, gaIndex);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -128,7 +216,11 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> open(BuildContext context, int index) async {
|
||||
Future<void> open(
|
||||
BuildContext context,
|
||||
List<MemoryItem> galleryItems,
|
||||
int index,
|
||||
) async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
|
|
@ -136,15 +228,9 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
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?;
|
||||
setState(() {});
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
}
|
||||
|
||||
orgMediaService.storedPath
|
||||
.copySync(newMediaService.tempPath.path);
|
||||
.copySync(newMediaService.originalPath.path);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AppearanceView> {
|
||||
bool showFeedbackShortcut = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
setState(() {
|
||||
showFeedbackShortcut = user.showFeedbackShortcut;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showSelectThemeMode(BuildContext context) async {
|
||||
|
|
@ -87,7 +77,29 @@ class _AppearanceViewState extends State<AppearanceView> {
|
|||
u.showFeedbackShortcut = !u.showFeedbackShortcut;
|
||||
return u;
|
||||
});
|
||||
await initAsync();
|
||||
setState(() {
|
||||
// gUser
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> toggleStartWithCameraOpen() async {
|
||||
await updateUserdata((u) {
|
||||
u.startWithCameraOpen = !u.startWithCameraOpen;
|
||||
return u;
|
||||
});
|
||||
setState(() {
|
||||
// gUser
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> toggleShowImagePreviewWhenSending() async {
|
||||
await updateUserdata((u) {
|
||||
u.showShowImagePreviewWhenSending = !u.showShowImagePreviewWhenSending;
|
||||
return u;
|
||||
});
|
||||
setState(() {
|
||||
// gUser
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -113,10 +125,26 @@ class _AppearanceViewState extends State<AppearanceView> {
|
|||
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(),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.lang.showImagePreviewWhenSending),
|
||||
onTap: toggleShowImagePreviewWhenSending,
|
||||
trailing: Switch(
|
||||
value: gUser.showShowImagePreviewWhenSending,
|
||||
onChanged: (a) => toggleShowImagePreviewWhenSending(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
67
pubspec.lock
67
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:
|
||||
|
|
@ -882,10 +887,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"
|
||||
|
|
@ -1244,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:
|
||||
|
|
@ -1532,14 +1535,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:
|
||||
|
|
|
|||
68
pubspec.yaml
68
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.80+80
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
|
|
@ -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,46 +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
|
||||
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
|
||||
|
||||
# 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
|
||||
|
|
@ -101,6 +106,9 @@ dependencies:
|
|||
mutex: ^3.1.0
|
||||
introduction_screen: ^4.0.0
|
||||
qr_flutter: ^4.1.0
|
||||
hand_signature: ^3.0.3
|
||||
flutter_sharing_intent: ^2.0.4
|
||||
no_screenshot: ^0.3.1
|
||||
|
||||
dependency_overrides:
|
||||
dots_indicator:
|
||||
|
|
@ -111,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:
|
||||
|
|
@ -123,6 +133,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:
|
||||
|
|
@ -133,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:
|
||||
|
|
@ -148,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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue