Merge pull request #300 from cogwheel0/native-context-menus-ios
native-context-menus-ios
This commit is contained in:
@@ -31,8 +31,8 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
# Prefer static frameworks to avoid Xcode embed-cycles with app extensions
|
||||
use_frameworks! :linkage => :static
|
||||
# Use dynamic frameworks for native extension compatibility
|
||||
use_frameworks! :linkage => :dynamic
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
@@ -62,12 +62,15 @@ post_install do |installer|
|
||||
native_target.build_configurations.each do |config|
|
||||
config.build_settings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'NO'
|
||||
|
||||
# Add -ObjC flag for objective_c package support in Release builds
|
||||
# Add -ObjC and -lc++ flags for native extensions support
|
||||
other_ldflags = config.build_settings['OTHER_LDFLAGS'] || ['$(inherited)']
|
||||
other_ldflags = [other_ldflags] if other_ldflags.is_a?(String)
|
||||
unless other_ldflags.include?('-ObjC')
|
||||
other_ldflags << '-ObjC'
|
||||
end
|
||||
unless other_ldflags.include?('-lc++')
|
||||
other_ldflags << '-lc++'
|
||||
end
|
||||
config.build_settings['OTHER_LDFLAGS'] = other_ldflags
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,8 @@ PODS:
|
||||
- CwlCatchException (2.2.1):
|
||||
- CwlCatchExceptionSupport (~> 2.2.1)
|
||||
- CwlCatchExceptionSupport (2.2.1)
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
@@ -58,6 +60,8 @@ PODS:
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- onnxruntime-c (1.22.0)
|
||||
- onnxruntime-objc (1.22.0):
|
||||
- onnxruntime-objc/Core (= 1.22.0)
|
||||
@@ -74,9 +78,9 @@ PODS:
|
||||
- Flutter
|
||||
- record_ios (1.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
- SDWebImage (5.21.5):
|
||||
- SDWebImage/Core (= 5.21.5)
|
||||
- SDWebImage/Core (5.21.5)
|
||||
- share_handler_ios (0.0.14):
|
||||
- Flutter
|
||||
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
||||
@@ -97,6 +101,8 @@ PODS:
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- super_native_extensions (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
@@ -112,6 +118,7 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`)
|
||||
@@ -121,6 +128,7 @@ DEPENDENCIES:
|
||||
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
@@ -132,6 +140,7 @@ DEPENDENCIES:
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- vad (from `.symlinks/plugins/vad/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
@@ -154,6 +163,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
@@ -172,6 +183,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
irondash_engine_context:
|
||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
pasteboard:
|
||||
@@ -194,6 +207,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/speech_to_text/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
super_native_extensions:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
vad:
|
||||
@@ -209,6 +224,7 @@ SPEC CHECKSUMS:
|
||||
CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90
|
||||
CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a
|
||||
CwlCatchExceptionSupport: 54ccab8d8c78907b57f99717fb19d4cc3bce02dc
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
@@ -220,6 +236,7 @@ SPEC CHECKSUMS:
|
||||
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
||||
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
@@ -227,19 +244,20 @@ SPEC CHECKSUMS:
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
|
||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
speech_to_text: 3b313d98516d3d0406cea424782ec25470c59d19
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
vad: 7934867589afe53567f492df66fb1615f2185822
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||
|
||||
PODFILE CHECKSUM: 32adf4606dae7e9ca2351c13b9e3ce1df6ad7ebf
|
||||
PODFILE CHECKSUM: a4b9cb970b890868c435231d99c6457dc39d3746
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
2C7FFDAA810DE8D0DE1C65C2 /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13BB6B75A265269EC0480713 /* Pods_ShareExtension.framework */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
55DE1EF6874060F683F7BE5A /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
925EEBF822EC5FC307568548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
A66B6643C89B7BB44E2B9471 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42FA90454A654BE3B4EA8A89 /* Pods_RunnerTests.framework */; };
|
||||
CA4A256DAFB6E4374BC67B91 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E952EFF81F1FEABA348FE2CC /* Pods_Runner.framework */; };
|
||||
F15AFEC42EE5499D00A1FABB /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F15AFEC32EE5499D00A1FABB /* WidgetKit.framework */; };
|
||||
F15AFEC62EE5499D00A1FABB /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F15AFEC52EE5499D00A1FABB /* SwiftUI.framework */; };
|
||||
F15AFED12EE5499E00A1FABB /* ConduitWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F15AFEC22EE5499D00A1FABB /* ConduitWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
F1DBCF1D2E601A39004C2540 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F1DBCF132E601A39004C2540 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
F1E401255BF7F4649BBEC0E4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */; };
|
||||
F1L10N012EE5500000000001 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = F1L10N002EE5500000000001 /* InfoPlist.strings */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -73,23 +73,23 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0EB032AD98424F9B253F06E2 /* 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>"; };
|
||||
13BB6B75A265269EC0480713 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
167D66D998116642A4545F97 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1EFA3C3B31029CE6405A8591 /* 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>"; };
|
||||
3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
17E0A3AD86D4871138027AA4 /* 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>"; };
|
||||
2A77EF27E68BB7EB54F177DB /* 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>"; };
|
||||
2AF06AFD3A8B2A85B9000E43 /* 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>"; };
|
||||
2C219D22481EC5F0F9827718 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; 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; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
3CBE9A216C715CD68229F6EC /* 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>"; };
|
||||
42FA90454A654BE3B4EA8A89 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
606D3073E5BC84DF85E7D8C2 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
6DEBCC1D4A97BA7E06B4F749 /* 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>"; };
|
||||
70EF9357577041094051C2F1 /* 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>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
81AA439A1413A5BB88E56615 /* 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>"; };
|
||||
8689E338A774C63931CCE3E4 /* 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>"; };
|
||||
951B4C2E3B8A60C543F47868 /* 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>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -97,9 +97,9 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C334EBA4AE824079ECAEE9EE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
CF1093DCAFB438AD6653A379 /* 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>"; };
|
||||
AEC6CFD537128217FAB31030 /* 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>"; };
|
||||
E590F0BC5DD9A3EFB1FDDDBF /* 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>"; };
|
||||
E952EFF81F1FEABA348FE2CC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F15AFEC22EE5499D00A1FABB /* ConduitWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ConduitWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F15AFEC32EE5499D00A1FABB /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
F15AFEC52EE5499D00A1FABB /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
@@ -167,7 +167,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F1E401255BF7F4649BBEC0E4 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
A66B6643C89B7BB44E2B9471 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -175,7 +175,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
925EEBF822EC5FC307568548 /* Pods_Runner.framework in Frameworks */,
|
||||
CA4A256DAFB6E4374BC67B91 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -192,7 +192,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
55DE1EF6874060F683F7BE5A /* Pods_ShareExtension.framework in Frameworks */,
|
||||
2C7FFDAA810DE8D0DE1C65C2 /* Pods_ShareExtension.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -210,11 +210,11 @@
|
||||
5C7D9D0FF0837699EA9220F9 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */,
|
||||
3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */,
|
||||
1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */,
|
||||
F15AFEC32EE5499D00A1FABB /* WidgetKit.framework */,
|
||||
F15AFEC52EE5499D00A1FABB /* SwiftUI.framework */,
|
||||
E952EFF81F1FEABA348FE2CC /* Pods_Runner.framework */,
|
||||
42FA90454A654BE3B4EA8A89 /* Pods_RunnerTests.framework */,
|
||||
13BB6B75A265269EC0480713 /* Pods_ShareExtension.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -222,15 +222,15 @@
|
||||
8C43905FA2E52A883F49D605 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0EB032AD98424F9B253F06E2 /* Pods-Runner.debug.xcconfig */,
|
||||
8689E338A774C63931CCE3E4 /* Pods-Runner.release.xcconfig */,
|
||||
C334EBA4AE824079ECAEE9EE /* Pods-Runner.profile.xcconfig */,
|
||||
951B4C2E3B8A60C543F47868 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
1EFA3C3B31029CE6405A8591 /* Pods-RunnerTests.release.xcconfig */,
|
||||
167D66D998116642A4545F97 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */,
|
||||
CF1093DCAFB438AD6653A379 /* Pods-ShareExtension.release.xcconfig */,
|
||||
3CBE9A216C715CD68229F6EC /* Pods-ShareExtension.profile.xcconfig */,
|
||||
6DEBCC1D4A97BA7E06B4F749 /* Pods-Runner.debug.xcconfig */,
|
||||
2A77EF27E68BB7EB54F177DB /* Pods-Runner.release.xcconfig */,
|
||||
2C219D22481EC5F0F9827718 /* Pods-Runner.profile.xcconfig */,
|
||||
2AF06AFD3A8B2A85B9000E43 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
E590F0BC5DD9A3EFB1FDDDBF /* Pods-RunnerTests.release.xcconfig */,
|
||||
606D3073E5BC84DF85E7D8C2 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
AEC6CFD537128217FAB31030 /* Pods-ShareExtension.debug.xcconfig */,
|
||||
17E0A3AD86D4871138027AA4 /* Pods-ShareExtension.release.xcconfig */,
|
||||
70EF9357577041094051C2F1 /* Pods-ShareExtension.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -296,7 +296,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
C9B5EBE3027E91A8CD4D83EE /* [CP] Check Pods Manifest.lock */,
|
||||
4BB2AA477F30D14C933273AD /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
3F0B71E2AB4D863902C09B3E /* Frameworks */,
|
||||
@@ -315,15 +315,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
0D14FD100D11330B7967BF40 /* [CP] Check Pods Manifest.lock */,
|
||||
DE631DFCBC56A5A8B5E0A4FD /* [CP] Check Pods Manifest.lock */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
F1DBCF1E2E601A39004C2540 /* Embed Foundation Extensions */,
|
||||
19E24AB51CA4721E8EB2EF0A /* [CP] Copy Pods Resources */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
24812099A551BA4570C59A8E /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -360,7 +360,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F1DBCF232E601A39004C2540 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
||||
buildPhases = (
|
||||
9B6E10307D13776C37514922 /* [CP] Check Pods Manifest.lock */,
|
||||
E39A68DFADF7E2F25C8E5C48 /* [CP] Check Pods Manifest.lock */,
|
||||
F1DBCF0F2E601A39004C2540 /* Sources */,
|
||||
F1DBCF102E601A39004C2540 /* Frameworks */,
|
||||
F1DBCF112E601A39004C2540 /* Resources */,
|
||||
@@ -470,7 +470,75 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
0D14FD100D11330B7967BF40 /* [CP] Check Pods Manifest.lock */ = {
|
||||
24812099A551BA4570C59A8E /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
4BB2AA477F30D14C933273AD /* [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-RunnerTests-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;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
DE631DFCBC56A5A8B5E0A4FD /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -492,53 +560,7 @@
|
||||
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;
|
||||
};
|
||||
19E24AB51CA4721E8EB2EF0A /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
9B6E10307D13776C37514922 /* [CP] Check Pods Manifest.lock */ = {
|
||||
E39A68DFADF7E2F25C8E5C48 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -560,28 +582,6 @@
|
||||
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;
|
||||
};
|
||||
C9B5EBE3027E91A8CD4D83EE /* [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-RunnerTests-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;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -748,6 +748,7 @@
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -760,7 +761,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 951B4C2E3B8A60C543F47868 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
baseConfigurationReference = 2AF06AFD3A8B2A85B9000E43 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -778,7 +779,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1EFA3C3B31029CE6405A8591 /* Pods-RunnerTests.release.xcconfig */;
|
||||
baseConfigurationReference = E590F0BC5DD9A3EFB1FDDDBF /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -794,7 +795,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 167D66D998116642A4545F97 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
baseConfigurationReference = 606D3073E5BC84DF85E7D8C2 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -942,6 +943,7 @@
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit.debug;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -976,6 +978,7 @@
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1120,7 +1123,7 @@
|
||||
};
|
||||
F1DBCF1F2E601A39004C2540 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */;
|
||||
baseConfigurationReference = AEC6CFD537128217FAB31030 /* Pods-ShareExtension.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@@ -1153,6 +1156,7 @@
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit.debug.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1167,7 +1171,7 @@
|
||||
};
|
||||
F1DBCF202E601A39004C2540 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = CF1093DCAFB438AD6653A379 /* Pods-ShareExtension.release.xcconfig */;
|
||||
baseConfigurationReference = 17E0A3AD86D4871138027AA4 /* Pods-ShareExtension.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@@ -1199,6 +1203,7 @@
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1211,7 +1216,7 @@
|
||||
};
|
||||
F1DBCF212E601A39004C2540 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3CBE9A216C715CD68229F6EC /* Pods-ShareExtension.profile.xcconfig */;
|
||||
baseConfigurationReference = 70EF9357577041094051C2F1 /* Pods-ShareExtension.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@@ -1243,6 +1248,7 @@
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -1570,16 +1570,14 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
// Keyboard visibility
|
||||
final keyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
|
||||
// Keyboard visibility - use viewInsetsOf for more efficient partial subscription
|
||||
final keyboardVisible = MediaQuery.viewInsetsOf(context).bottom > 0;
|
||||
// Whether the messages list can actually scroll (avoids showing button when not needed)
|
||||
final canScroll =
|
||||
_scrollController.hasClients &&
|
||||
_scrollController.position.maxScrollExtent > 0;
|
||||
// Check if any message is currently streaming (for scroll button indicator)
|
||||
final isStreamingAnyMessage = ref
|
||||
.watch(chatMessagesProvider)
|
||||
.any((msg) => msg.isStreaming);
|
||||
// Use dedicated streaming provider to avoid iterating all messages on rebuild
|
||||
final isStreamingAnyMessage = ref.watch(isChatStreamingProvider);
|
||||
|
||||
// On keyboard open, if already near bottom, auto-scroll to bottom to keep input visible
|
||||
if (keyboardVisible && !_lastKeyboardVisible) {
|
||||
@@ -1601,15 +1599,12 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
});
|
||||
}
|
||||
|
||||
// Focus composer on app startup once
|
||||
// Focus composer on app startup once (minimal delay for layout to settle)
|
||||
if (!_didStartupFocus) {
|
||||
_didStartupFocus = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (!mounted) return;
|
||||
final current = ref.read(inputFocusTriggerProvider);
|
||||
ref.read(inputFocusTriggerProvider.notifier).set(current + 1);
|
||||
});
|
||||
if (!mounted) return;
|
||||
ref.read(inputFocusTriggerProvider.notifier).increment();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1817,18 +1812,15 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
),
|
||||
);
|
||||
} else if (displayConversationTitle != null) {
|
||||
titlePill = GestureDetector(
|
||||
onTap: () {
|
||||
final conversation = ref.read(
|
||||
activeConversationProvider,
|
||||
);
|
||||
if (conversation == null) return;
|
||||
showConversationContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
conversation: conversation,
|
||||
);
|
||||
},
|
||||
final conversation = ref.read(
|
||||
activeConversationProvider,
|
||||
);
|
||||
titlePill = ConduitContextMenu(
|
||||
actions: buildConversationActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
conversation: conversation,
|
||||
),
|
||||
child: _buildAppBarPill(
|
||||
context: context,
|
||||
child: Padding(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
// app_theme not required here; using theme extension tokens
|
||||
@@ -183,13 +184,23 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
}
|
||||
|
||||
_pendingFocus = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
// Request focus synchronously if we're already in a safe context,
|
||||
// otherwise defer to next frame
|
||||
if (WidgetsBinding.instance.schedulerPhase ==
|
||||
SchedulerPhase.persistentCallbacks) {
|
||||
// We're in a build/layout phase, defer to next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_pendingFocus = false;
|
||||
if (widget.enabled && !_focusNode.hasFocus) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Safe to request focus immediately
|
||||
_pendingFocus = false;
|
||||
if (widget.enabled && !_focusNode.hasFocus) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1032,11 +1043,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
});
|
||||
});
|
||||
|
||||
final messages = ref.watch(chatMessagesProvider);
|
||||
final isGenerating =
|
||||
messages.isNotEmpty &&
|
||||
messages.last.role == 'assistant' &&
|
||||
messages.last.isStreaming;
|
||||
// Use dedicated streaming provider to avoid rebuilding on every message change
|
||||
final isGenerating = ref.watch(isChatStreamingProvider);
|
||||
final stopGeneration = ref.read(stopGenerationProvider);
|
||||
|
||||
final webSearchEnabled = ref.watch(webSearchEnabledProvider);
|
||||
@@ -1340,9 +1348,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
// For compact mode, render text field shell with floating buttons on sides
|
||||
if (showCompactComposer) {
|
||||
// Build the text field shell
|
||||
Widget textFieldShell = AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
Widget textFieldShell = Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||
constraints: const BoxConstraints(minHeight: TouchTarget.input),
|
||||
decoration: shellDecoration,
|
||||
@@ -1404,9 +1410,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
}
|
||||
|
||||
// For expanded mode with quick pills, use the full shell
|
||||
Widget shell = AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
Widget shell = Container(
|
||||
decoration: shellDecoration,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
|
||||
@@ -433,51 +433,32 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _showMessageMenu(BuildContext context) async {
|
||||
// Don't show menu while editing - use the visible Save/Cancel buttons instead
|
||||
if (_isEditing) return;
|
||||
List<ConduitContextMenuAction> _buildMessageActions(BuildContext context) {
|
||||
// Don't show menu while editing - return empty list
|
||||
if (_isEditing) return [];
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
// Get the position of the bubble to show menu below it
|
||||
Offset? menuPosition;
|
||||
final RenderBox? renderBox =
|
||||
_bubbleKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
final size = renderBox.size;
|
||||
// Position menu at bottom-right of the bubble
|
||||
menuPosition = Offset(
|
||||
position.dx + size.width,
|
||||
position.dy + size.height,
|
||||
);
|
||||
}
|
||||
|
||||
await showConduitContextMenu(
|
||||
context: context,
|
||||
position: menuPosition,
|
||||
actions: [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async => _startInlineEdit(),
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.doc_on_clipboard,
|
||||
materialIcon: Icons.content_copy,
|
||||
label: l10n.copy,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
if (widget.onCopy != null) {
|
||||
widget.onCopy!();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async => _startInlineEdit(),
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.doc_on_clipboard,
|
||||
materialIcon: Icons.content_copy,
|
||||
label: l10n.copy,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
if (widget.onCopy != null) {
|
||||
widget.onCopy!();
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -498,10 +479,15 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
|
||||
final inlineEditFill = context.conduitTheme.surfaceContainer.withValues(
|
||||
alpha: 0.92,
|
||||
);
|
||||
// Use rounded rectangle for multiline, pill for single-line (like chat input)
|
||||
// Consider multiline if text exceeds ~50 chars or contains newlines
|
||||
// Check length first (O(1)) to short-circuit before scanning for newlines
|
||||
final content = widget.message.content;
|
||||
final isMultiline = content.length > 50 || content.contains('\n');
|
||||
final bubbleRadius = isMultiline ? AppBorderRadius.xl : AppBorderRadius.pill;
|
||||
|
||||
return GestureDetector(
|
||||
onLongPress: () => _showMessageMenu(context),
|
||||
behavior: HitTestBehavior.translucent,
|
||||
return ConduitContextMenu(
|
||||
actions: _buildMessageActions(context),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(
|
||||
@@ -539,9 +525,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.chatBubbleUser,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.messageBubble,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(bubbleRadius),
|
||||
),
|
||||
child: _isEditing
|
||||
? Focus(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:super_drag_and_drop/super_drag_and_drop.dart';
|
||||
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
import '../../auth/providers/unified_auth_providers.dart';
|
||||
@@ -18,11 +19,9 @@ import '../../../shared/widgets/loading_states.dart';
|
||||
import '../../../shared/widgets/themed_dialogs.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import '../../../core/utils/user_display_name.dart';
|
||||
import '../../../core/utils/model_icon_utils.dart';
|
||||
import '../../../core/utils/user_avatar_utils.dart';
|
||||
import '../../../shared/utils/conversation_context_menu.dart';
|
||||
import '../../../shared/widgets/user_avatar.dart';
|
||||
import '../../../shared/widgets/model_avatar.dart';
|
||||
import '../../../shared/widgets/conduit_components.dart';
|
||||
import '../../../shared/widgets/responsive_drawer_layout.dart';
|
||||
import '../../../core/models/model.dart';
|
||||
@@ -182,9 +181,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
child: Stack(
|
||||
children: [
|
||||
// Main scrollable content - extends behind floating elements
|
||||
Positioned.fill(
|
||||
child: _buildConversationList(context),
|
||||
),
|
||||
Positioned.fill(child: _buildConversationList(context)),
|
||||
// Floating top area with gradient background (matches app bar pattern)
|
||||
Positioned(
|
||||
top: 0,
|
||||
@@ -241,7 +238,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final bottomPadding = MediaQuery.of(context).viewPadding.bottom;
|
||||
final bottomPadding = MediaQuery.of(
|
||||
context,
|
||||
).viewPadding.bottom;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -1003,26 +1002,50 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
||||
final isExpanded = expandedMap[folderId] ?? defaultExpanded;
|
||||
final isHover = _dragHoverFolderId == folderId;
|
||||
return DragTarget<_DragConversationData>(
|
||||
onWillAcceptWithDetails: (details) {
|
||||
final baseColor = theme.surfaceContainer;
|
||||
final hoverColor = theme.buttonPrimary.withValues(alpha: 0.08);
|
||||
final borderColor = isHover
|
||||
? theme.buttonPrimary.withValues(alpha: 0.60)
|
||||
: theme.surfaceContainerHighest.withValues(alpha: 0.40);
|
||||
|
||||
Color? overlayForStates(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return theme.buttonPrimary.withValues(alpha: Alpha.buttonPressed);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered) ||
|
||||
states.contains(WidgetState.focused)) {
|
||||
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
|
||||
}
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
return DropRegion(
|
||||
formats: const [], // Local data only
|
||||
onDropOver: (event) {
|
||||
setState(() => _dragHoverFolderId = folderId);
|
||||
return true;
|
||||
return DropOperation.move;
|
||||
},
|
||||
onLeave: (_) => setState(() => _dragHoverFolderId = null),
|
||||
onAcceptWithDetails: (details) async {
|
||||
onDropEnter: (_) => setState(() => _dragHoverFolderId = folderId),
|
||||
onDropLeave: (_) => setState(() => _dragHoverFolderId = null),
|
||||
onPerformDrop: (event) async {
|
||||
setState(() {
|
||||
_dragHoverFolderId = null;
|
||||
_isDragging = false;
|
||||
});
|
||||
// Get local data from the drop event (serialized as Map)
|
||||
final localData = event.session.items.first.localData;
|
||||
if (localData is! Map) return;
|
||||
final conversationId = localData['id'] as String?;
|
||||
if (conversationId == null) return;
|
||||
try {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
if (api == null) throw Exception('No API service');
|
||||
await api.moveConversationToFolder(details.data.id, folderId);
|
||||
await api.moveConversationToFolder(conversationId, folderId);
|
||||
HapticFeedback.selectionClick();
|
||||
ref
|
||||
.read(conversationsProvider.notifier)
|
||||
.updateConversation(
|
||||
details.data.id,
|
||||
conversationId,
|
||||
(conversation) => conversation.copyWith(
|
||||
folderId: folderId,
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -1043,25 +1066,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
final baseColor = theme.surfaceContainer;
|
||||
final hoverColor = theme.buttonPrimary.withValues(alpha: 0.08);
|
||||
final borderColor = isHover
|
||||
? theme.buttonPrimary.withValues(alpha: 0.60)
|
||||
: theme.surfaceContainerHighest.withValues(alpha: 0.40);
|
||||
|
||||
Color? overlayForStates(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return theme.buttonPrimary.withValues(alpha: Alpha.buttonPressed);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered) ||
|
||||
states.contains(WidgetState.focused)) {
|
||||
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
|
||||
}
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
return Material(
|
||||
child: ConduitContextMenu(
|
||||
actions: _buildFolderActions(folderId, name),
|
||||
child: Material(
|
||||
color: isHover ? hoverColor : baseColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
||||
@@ -1075,10 +1082,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
current[folderId] = next;
|
||||
ref.read(_expandedFoldersProvider.notifier).set(current);
|
||||
},
|
||||
onLongPress: () {
|
||||
HapticFeedback.selectionClick();
|
||||
_showFolderContextMenu(context, folderId, name);
|
||||
},
|
||||
onLongPress: null, // Handled by ConduitContextMenu
|
||||
overlayColor: WidgetStateProperty.resolveWith(overlayForStates),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
@@ -1136,10 +1140,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.sidebarTheme.accent
|
||||
.withValues(alpha: 0.7),
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppBorderRadius.xs),
|
||||
color: context.sidebarTheme.accent.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.xs,
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.sidebarTheme.border
|
||||
.withValues(alpha: 0.35),
|
||||
@@ -1202,8 +1208,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1296,37 +1302,33 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showFolderContextMenu(
|
||||
BuildContext context,
|
||||
List<ConduitContextMenuAction> _buildFolderActions(
|
||||
String folderId,
|
||||
String folderName,
|
||||
) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
showConduitContextMenu(
|
||||
context: context,
|
||||
actions: [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_rounded,
|
||||
label: l10n.rename,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
await _renameFolder(context, folderId, folderName);
|
||||
},
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.delete,
|
||||
materialIcon: Icons.delete_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onBeforeClose: () => HapticFeedback.mediumImpact(),
|
||||
onSelected: () async {
|
||||
await _confirmAndDeleteFolder(context, folderId, folderName);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_rounded,
|
||||
label: l10n.rename,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
await _renameFolder(context, folderId, folderName);
|
||||
},
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.delete,
|
||||
materialIcon: Icons.delete_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onBeforeClose: () => HapticFeedback.mediumImpact(),
|
||||
onSelected: () async {
|
||||
await _confirmAndDeleteFolder(context, folderId, folderName);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _startNewChatInFolder(String folderId) {
|
||||
@@ -1433,26 +1435,33 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
final theme = context.conduitTheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final isHover = _dragHoverFolderId == '__UNFILE__';
|
||||
return DragTarget<_DragConversationData>(
|
||||
onWillAcceptWithDetails: (details) {
|
||||
return DropRegion(
|
||||
formats: const [], // Local data only
|
||||
onDropOver: (event) {
|
||||
setState(() => _dragHoverFolderId = '__UNFILE__');
|
||||
return true;
|
||||
return DropOperation.move;
|
||||
},
|
||||
onLeave: (_) => setState(() => _dragHoverFolderId = null),
|
||||
onAcceptWithDetails: (details) async {
|
||||
onDropEnter: (_) => setState(() => _dragHoverFolderId = '__UNFILE__'),
|
||||
onDropLeave: (_) => setState(() => _dragHoverFolderId = null),
|
||||
onPerformDrop: (event) async {
|
||||
setState(() {
|
||||
_dragHoverFolderId = null;
|
||||
_isDragging = false;
|
||||
});
|
||||
// Get local data from the drop event (serialized as Map)
|
||||
final localData = event.session.items.first.localData;
|
||||
if (localData is! Map) return;
|
||||
final conversationId = localData['id'] as String?;
|
||||
if (conversationId == null) return;
|
||||
try {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
if (api == null) throw Exception('No API service');
|
||||
await api.moveConversationToFolder(details.data.id, null);
|
||||
await api.moveConversationToFolder(conversationId, null);
|
||||
HapticFeedback.selectionClick();
|
||||
ref
|
||||
.read(conversationsProvider.notifier)
|
||||
.updateConversation(
|
||||
details.data.id,
|
||||
conversationId,
|
||||
(conversation) => conversation.copyWith(
|
||||
folderId: null,
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -1471,45 +1480,43 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, candidate, rejected) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 120),
|
||||
decoration: BoxDecoration(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 120),
|
||||
decoration: BoxDecoration(
|
||||
color: isHover
|
||||
? theme.buttonPrimary.withValues(alpha: 0.08)
|
||||
: theme.surfaceContainer.withValues(alpha: 0.03),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
||||
border: Border.all(
|
||||
color: isHover
|
||||
? theme.buttonPrimary.withValues(alpha: 0.08)
|
||||
: theme.surfaceContainer.withValues(alpha: 0.03),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
||||
border: Border.all(
|
||||
color: isHover
|
||||
? theme.buttonPrimary.withValues(alpha: 0.5)
|
||||
: theme.dividerColor.withValues(alpha: 0.5),
|
||||
width: BorderWidth.standard,
|
||||
),
|
||||
? theme.buttonPrimary.withValues(alpha: 0.5)
|
||||
: theme.dividerColor.withValues(alpha: 0.5),
|
||||
width: BorderWidth.standard,
|
||||
),
|
||||
padding: const EdgeInsets.all(Spacing.sm),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.folder_badge_minus
|
||||
: Icons.folder_off_outlined,
|
||||
color: theme.iconPrimary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Drop here to remove from folder',
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(Spacing.sm),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.folder_badge_minus
|
||||
: Icons.folder_off_outlined,
|
||||
color: theme.iconPrimary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Drop here to remove from folder',
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1529,81 +1536,84 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
(ref.watch(chat.isLoadingConversationProvider) == true);
|
||||
final bool isPinned = conv.pinned == true;
|
||||
|
||||
Model? model;
|
||||
final modelId = (conv.model is String && (conv.model as String).isNotEmpty)
|
||||
? conv.model as String
|
||||
: null;
|
||||
if (modelId != null) {
|
||||
model = modelsById[modelId];
|
||||
}
|
||||
// Check if folders feature is enabled to enable drag
|
||||
final foldersEnabled = ref.watch(foldersFeatureEnabledProvider);
|
||||
final dragEnabled = foldersEnabled && !isLoadingSelected;
|
||||
|
||||
final api = ref.watch(apiServiceProvider);
|
||||
final modelIconUrl = resolveModelIconUrlForModel(api, model);
|
||||
|
||||
Widget? leading;
|
||||
if (modelId != null) {
|
||||
leading = ModelAvatar(
|
||||
size: 28,
|
||||
imageUrl: modelIconUrl,
|
||||
label: model?.name ?? modelId,
|
||||
);
|
||||
}
|
||||
|
||||
final tile = _ConversationTile(
|
||||
final tileWidget = _ConversationTile(
|
||||
title: title,
|
||||
pinned: isPinned,
|
||||
selected: isActive,
|
||||
isLoading: isLoadingSelected,
|
||||
leading: leading,
|
||||
onTap: _isLoadingConversation
|
||||
? null
|
||||
: () => _selectConversation(context, conv.id),
|
||||
onLongPress: null,
|
||||
onMorePressed: (buttonContext) {
|
||||
showConversationContextMenu(
|
||||
context: buttonContext,
|
||||
ref: ref,
|
||||
conversation: conv,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return RepaintBoundary(
|
||||
final contextMenuTile = ConduitContextMenu(
|
||||
actions: buildConversationActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
conversation: conv,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Spacing.xs,
|
||||
left: inFolder ? Spacing.md : 0,
|
||||
),
|
||||
child: LongPressDraggable<_DragConversationData>(
|
||||
data: _DragConversationData(id: conv.id, title: title),
|
||||
dragAnchorStrategy: pointerDragAnchorStrategy,
|
||||
feedback: _ConversationDragFeedback(
|
||||
title: title,
|
||||
pinned: isPinned,
|
||||
theme: theme,
|
||||
),
|
||||
childWhenDragging: Opacity(
|
||||
opacity: 0.5,
|
||||
child: IgnorePointer(child: tile),
|
||||
),
|
||||
onDragStarted: () {
|
||||
HapticFeedback.lightImpact();
|
||||
final hasFolder =
|
||||
(conv.folderId != null && (conv.folderId as String).isNotEmpty);
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
_draggingHasFolder = hasFolder;
|
||||
});
|
||||
},
|
||||
onDragEnd: (_) => setState(() {
|
||||
_dragHoverFolderId = null;
|
||||
_isDragging = false;
|
||||
_draggingHasFolder = false;
|
||||
}),
|
||||
child: tile,
|
||||
),
|
||||
padding: EdgeInsets.only(left: inFolder ? Spacing.sm : 0),
|
||||
child: tileWidget,
|
||||
),
|
||||
);
|
||||
|
||||
// Wrap with drag support if folders are enabled
|
||||
Widget tile;
|
||||
if (dragEnabled) {
|
||||
tile = DragItemWidget(
|
||||
allowedOperations: () => [DropOperation.move],
|
||||
canAddItemToExistingSession: true,
|
||||
dragItemProvider: (request) async {
|
||||
// Set drag state when drag starts
|
||||
HapticFeedback.lightImpact();
|
||||
final hasFolder =
|
||||
(conv.folderId != null && (conv.folderId as String).isNotEmpty);
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
_draggingHasFolder = hasFolder;
|
||||
});
|
||||
|
||||
// Listen for drag completion to reset state
|
||||
void onDragCompleted() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_dragHoverFolderId = null;
|
||||
_isDragging = false;
|
||||
_draggingHasFolder = false;
|
||||
});
|
||||
}
|
||||
request.session.dragCompleted.removeListener(onDragCompleted);
|
||||
}
|
||||
|
||||
request.session.dragCompleted.addListener(onDragCompleted);
|
||||
|
||||
// Provide drag data with conversation info as serializable Map
|
||||
final item = DragItem(localData: {'id': conv.id, 'title': title});
|
||||
return item;
|
||||
},
|
||||
dragBuilder: (context, child) {
|
||||
// Custom drag preview
|
||||
return Opacity(
|
||||
opacity: 0.9,
|
||||
child: _ConversationDragFeedback(
|
||||
title: title,
|
||||
pinned: isPinned,
|
||||
theme: theme,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: DraggableWidget(child: contextMenuTile),
|
||||
);
|
||||
} else {
|
||||
tile = contextMenuTile;
|
||||
}
|
||||
|
||||
return RepaintBoundary(child: tile);
|
||||
}
|
||||
|
||||
Widget _buildArchivedHeader(int count) {
|
||||
@@ -1790,9 +1800,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.avatar,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
|
||||
border: Border.all(
|
||||
color: conduitTheme.buttonPrimary.withValues(alpha: 0.25),
|
||||
width: BorderWidth.thin,
|
||||
@@ -1916,12 +1924,6 @@ class _ExpandedFoldersNotifier extends Notifier<Map<String, bool>> {
|
||||
void set(Map<String, bool> value) => state = Map<String, bool>.from(value);
|
||||
}
|
||||
|
||||
class _DragConversationData {
|
||||
final String id;
|
||||
final String title;
|
||||
const _DragConversationData({required this.id, required this.title});
|
||||
}
|
||||
|
||||
class _ConversationDragFeedback extends StatelessWidget {
|
||||
final String title;
|
||||
final bool pinned;
|
||||
@@ -1958,7 +1960,6 @@ class _ConversationDragFeedback extends StatelessWidget {
|
||||
pinned: pinned,
|
||||
selected: false,
|
||||
isLoading: false,
|
||||
onMorePressed: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1970,23 +1971,21 @@ class _ConversationTileContent extends StatelessWidget {
|
||||
final bool pinned;
|
||||
final bool selected;
|
||||
final bool isLoading;
|
||||
final void Function(BuildContext)? onMorePressed;
|
||||
final Widget? leading;
|
||||
|
||||
const _ConversationTileContent({
|
||||
required this.title,
|
||||
required this.pinned,
|
||||
required this.selected,
|
||||
required this.isLoading,
|
||||
this.onMorePressed,
|
||||
this.leading,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.conduitTheme;
|
||||
|
||||
// Enhanced typography with better visual hierarchy
|
||||
final textStyle = AppTypography.standard.copyWith(
|
||||
color: theme.textPrimary,
|
||||
color: selected ? theme.textPrimary : theme.textSecondary,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
||||
height: 1.4,
|
||||
);
|
||||
@@ -1996,20 +1995,30 @@ class _ConversationTileContent extends StatelessWidget {
|
||||
final hasFiniteWidth = constraints.maxWidth.isFinite;
|
||||
final textFit = hasFiniteWidth ? FlexFit.tight : FlexFit.loose;
|
||||
|
||||
final trailing = <Widget>[];
|
||||
final trailingWidgets = <Widget>[];
|
||||
|
||||
if (pinned) {
|
||||
trailing.addAll([
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Icon(
|
||||
Platform.isIOS ? CupertinoIcons.pin_fill : Icons.push_pin_rounded,
|
||||
color: theme.iconSecondary,
|
||||
size: IconSize.xs,
|
||||
trailingWidgets.addAll([
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(Spacing.xxs),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.buttonPrimary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.pin_fill
|
||||
: Icons.push_pin_rounded,
|
||||
color: theme.buttonPrimary.withValues(alpha: 0.7),
|
||||
size: IconSize.xs,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
trailing.addAll([
|
||||
trailingWidgets.addAll([
|
||||
const SizedBox(width: Spacing.sm),
|
||||
SizedBox(
|
||||
width: IconSize.sm,
|
||||
@@ -2022,47 +2031,11 @@ class _ConversationTileContent extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
]);
|
||||
} else if (onMorePressed != null) {
|
||||
trailing.addAll([
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Builder(
|
||||
builder: (buttonContext) {
|
||||
return IconButton(
|
||||
iconSize: IconSize.sm,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: TouchTarget.listItem,
|
||||
minHeight: TouchTarget.listItem,
|
||||
),
|
||||
icon: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.ellipsis
|
||||
: Icons.more_vert_rounded,
|
||||
color: theme.iconSecondary,
|
||||
),
|
||||
onPressed: () => onMorePressed!(buttonContext),
|
||||
tooltip: AppLocalizations.of(context)!.more,
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: hasFiniteWidth ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: [
|
||||
if (leading != null) ...[
|
||||
SizedBox(
|
||||
width: TouchTarget.listItem,
|
||||
height: TouchTarget.listItem,
|
||||
child: Center(child: leading!),
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
],
|
||||
Flexible(
|
||||
fit: textFit,
|
||||
child: MiddleEllipsisText(
|
||||
@@ -2071,7 +2044,7 @@ class _ConversationTileContent extends StatelessWidget {
|
||||
semanticsLabel: title,
|
||||
),
|
||||
),
|
||||
...trailing,
|
||||
...trailingWidgets,
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -2079,98 +2052,103 @@ class _ConversationTileContent extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ConversationTile extends StatelessWidget {
|
||||
class _ConversationTile extends StatefulWidget {
|
||||
final String title;
|
||||
final bool pinned;
|
||||
final bool selected;
|
||||
final bool isLoading;
|
||||
final Widget? leading;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final void Function(BuildContext)? onMorePressed;
|
||||
|
||||
const _ConversationTile({
|
||||
required this.title,
|
||||
required this.pinned,
|
||||
required this.selected,
|
||||
required this.isLoading,
|
||||
this.leading,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
this.onMorePressed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ConversationTile> createState() => _ConversationTileState();
|
||||
}
|
||||
|
||||
class _ConversationTileState extends State<_ConversationTile> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.conduitTheme;
|
||||
const BorderRadius borderRadius = BorderRadius.zero;
|
||||
final Color background = selected
|
||||
? theme.buttonPrimary.withValues(alpha: 0.1)
|
||||
: Colors.transparent;
|
||||
final Color borderColor = selected
|
||||
? theme.buttonPrimary.withValues(alpha: 0.5)
|
||||
: Colors.transparent;
|
||||
final sidebarTheme = context.sidebarTheme;
|
||||
final borderRadius = BorderRadius.circular(AppBorderRadius.sm);
|
||||
|
||||
final List<BoxShadow> shadow = const [];
|
||||
// Use opaque backgrounds for proper context menu snapshot rendering
|
||||
final Color baseBackground = sidebarTheme.background;
|
||||
|
||||
final Color background = widget.selected
|
||||
? Color.alphaBlend(
|
||||
theme.buttonPrimary.withValues(alpha: 0.1),
|
||||
baseBackground,
|
||||
)
|
||||
: (_isHovered
|
||||
? Color.alphaBlend(
|
||||
theme.buttonPrimary.withValues(alpha: 0.05),
|
||||
baseBackground,
|
||||
)
|
||||
: baseBackground);
|
||||
|
||||
// Border styling
|
||||
final Color borderColor = widget.selected
|
||||
? theme.buttonPrimary.withValues(alpha: 0.4)
|
||||
: Colors.transparent;
|
||||
|
||||
Color? overlayForStates(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return theme.buttonPrimary.withValues(alpha: Alpha.buttonPressed);
|
||||
}
|
||||
if (states.contains(WidgetState.focused) ||
|
||||
states.contains(WidgetState.hovered)) {
|
||||
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
|
||||
}
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
selected: selected,
|
||||
selected: widget.selected,
|
||||
button: true,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(borderRadius: borderRadius),
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: isLoading ? null : onTap,
|
||||
onLongPress: onLongPress,
|
||||
overlayColor: WidgetStateProperty.resolveWith(overlayForStates),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
curve: Curves.easeOut,
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.xs,
|
||||
vertical: Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
borderRadius: borderRadius,
|
||||
border: widget.selected
|
||||
? Border.all(color: borderColor, width: BorderWidth.regular)
|
||||
: null,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: borderRadius,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
border: selected
|
||||
? Border(
|
||||
top: BorderSide(
|
||||
color: borderColor,
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: borderColor,
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
boxShadow: shadow,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: TouchTarget.listItem,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.xs,
|
||||
onTap: widget.isLoading ? null : widget.onTap,
|
||||
overlayColor: WidgetStateProperty.resolveWith(overlayForStates),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: TouchTarget.listItem,
|
||||
),
|
||||
child: _ConversationTileContent(
|
||||
title: title,
|
||||
pinned: pinned,
|
||||
selected: selected,
|
||||
isLoading: isLoading,
|
||||
onMorePressed: onMorePressed,
|
||||
leading: leading,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.sm,
|
||||
),
|
||||
child: _ConversationTileContent(
|
||||
title: widget.title,
|
||||
pinned: widget.pinned,
|
||||
selected: widget.selected,
|
||||
isLoading: widget.isLoading,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -709,39 +709,50 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Actions (more menu)
|
||||
// Actions (more menu) - uses PopupMenuButton for tap interaction
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: Spacing.inputPadding),
|
||||
child: Center(
|
||||
child: PopupMenuButton<String>(
|
||||
tooltip: '',
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'generate_title':
|
||||
case 'generate':
|
||||
HapticFeedback.selectionClick();
|
||||
_generateTitle();
|
||||
case 'copy':
|
||||
HapticFeedback.selectionClick();
|
||||
_copyToClipboard();
|
||||
case 'delete':
|
||||
HapticFeedback.mediumImpact();
|
||||
_deleteNote();
|
||||
}
|
||||
},
|
||||
offset: const Offset(0, 44),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.card,
|
||||
),
|
||||
),
|
||||
color: conduitTheme.surfaceContainer,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'generate_title',
|
||||
value: 'generate',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.sparkles
|
||||
: Icons.auto_awesome_rounded,
|
||||
color: conduitTheme.buttonPrimary,
|
||||
size: IconSize.md,
|
||||
size: IconSize.small,
|
||||
color: conduitTheme.textPrimary,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Text(l10n.generateTitle),
|
||||
Text(
|
||||
l10n.generateTitle,
|
||||
style: TextStyle(
|
||||
color: conduitTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -753,11 +764,16 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.doc_on_clipboard
|
||||
: Icons.copy_rounded,
|
||||
color: conduitTheme.iconPrimary,
|
||||
size: IconSize.md,
|
||||
size: IconSize.small,
|
||||
color: conduitTheme.textPrimary,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Text(l10n.copy),
|
||||
Text(
|
||||
l10n.copy,
|
||||
style: TextStyle(
|
||||
color: conduitTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -769,13 +785,15 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.delete
|
||||
: Icons.delete_rounded,
|
||||
size: IconSize.small,
|
||||
color: conduitTheme.error,
|
||||
size: IconSize.md,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Text(
|
||||
l10n.delete,
|
||||
style: TextStyle(color: conduitTheme.error),
|
||||
style: TextStyle(
|
||||
color: conduitTheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -823,17 +841,13 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
|
||||
}
|
||||
|
||||
Widget _buildFloatingMetadataBar(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final conduitTheme = context.conduitTheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
final backgroundColor = isDark
|
||||
? Color.lerp(conduitTheme.cardBackground, Colors.white, 0.08)!
|
||||
: Color.lerp(conduitTheme.inputBackground, Colors.black, 0.06)!;
|
||||
|
||||
final borderColor = conduitTheme.cardBorder.withValues(
|
||||
alpha: isDark ? 0.65 : 0.55,
|
||||
// Use consistent colors with the floating app bar pills
|
||||
final backgroundColor = conduitTheme.surfaceContainer.withValues(alpha: 0.9);
|
||||
final borderColor = conduitTheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.4,
|
||||
);
|
||||
|
||||
final dateFormat = DateFormat.MMMd();
|
||||
@@ -898,7 +912,7 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
|
||||
child: Text(
|
||||
'·',
|
||||
style: AppTypography.tiny.copyWith(
|
||||
color: theme.textTertiary.withValues(alpha: 0.5),
|
||||
color: theme.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -921,14 +935,14 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: theme.textTertiary.withValues(alpha: 0.7),
|
||||
color: theme.textSecondary,
|
||||
size: IconSize.xs,
|
||||
),
|
||||
const SizedBox(width: Spacing.xxs),
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.tiny.copyWith(
|
||||
color: theme.textTertiary.withValues(alpha: 0.7),
|
||||
color: theme.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -414,33 +414,40 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: Spacing.sm),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: sidebarTheme.accent.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
||||
border: Border.all(
|
||||
color: sidebarTheme.border.withValues(alpha: 0.15),
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Compute opaque background for proper context menu snapshot rendering
|
||||
final cardBackground = Color.alphaBlend(
|
||||
sidebarTheme.accent.withValues(alpha: 0.5),
|
||||
sidebarTheme.background,
|
||||
);
|
||||
|
||||
return ConduitContextMenu(
|
||||
actions: _buildNoteActions(context, note),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: Spacing.sm),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
color: cardBackground,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
||||
child: InkWell(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
||||
border: Border.all(
|
||||
color: sidebarTheme.border.withValues(alpha: 0.15),
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
||||
overlayColor: WidgetStateProperty.resolveWith(overlayForStates),
|
||||
onTap: () {
|
||||
@@ -450,7 +457,7 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
|
||||
pathParameters: {'id': note.id},
|
||||
);
|
||||
},
|
||||
onLongPress: () => _showNoteContextMenu(context, note),
|
||||
onLongPress: null, // Handled by ConduitContextMenu
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Spacing.md),
|
||||
child: Row(
|
||||
@@ -558,32 +565,13 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// More button
|
||||
Builder(
|
||||
builder: (buttonContext) => IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.ellipsis
|
||||
: Icons.more_vert_rounded,
|
||||
color: sidebarTheme.foreground.withValues(alpha: 0.5),
|
||||
size: IconSize.md,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: TouchTarget.badge,
|
||||
minHeight: TouchTarget.badge,
|
||||
),
|
||||
onPressed: () =>
|
||||
_showNoteContextMenu(buttonContext, note),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -594,51 +582,51 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
|
||||
date.day == now.day;
|
||||
}
|
||||
|
||||
void _showNoteContextMenu(BuildContext context, Note note) {
|
||||
List<ConduitContextMenuAction> _buildNoteActions(
|
||||
BuildContext context,
|
||||
Note note,
|
||||
) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
showConduitContextMenu(
|
||||
context: context,
|
||||
actions: [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_rounded,
|
||||
label: l10n.edit,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
context.pushNamed(
|
||||
RouteNames.noteEditor,
|
||||
pathParameters: {'id': note.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.doc_on_clipboard,
|
||||
materialIcon: Icons.copy_rounded,
|
||||
label: l10n.copy,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await Clipboard.setData(ClipboardData(text: note.markdownContent));
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.noteCopiedToClipboard),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.delete,
|
||||
materialIcon: Icons.delete_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onBeforeClose: () => HapticFeedback.mediumImpact(),
|
||||
onSelected: () async => _deleteNote(note),
|
||||
),
|
||||
],
|
||||
);
|
||||
return [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_rounded,
|
||||
label: l10n.edit,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
context.pushNamed(
|
||||
RouteNames.noteEditor,
|
||||
pathParameters: {'id': note.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.doc_on_clipboard,
|
||||
materialIcon: Icons.copy_rounded,
|
||||
label: l10n.copy,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await Clipboard.setData(ClipboardData(text: note.markdownContent));
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.noteCopiedToClipboard),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.delete,
|
||||
materialIcon: Icons.delete_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onBeforeClose: () => HapticFeedback.mediumImpact(),
|
||||
onSelected: () async => _deleteNote(note),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
|
||||
@@ -243,6 +243,28 @@ class AppTheme {
|
||||
iconColor: tokens.neutralTone80,
|
||||
textColor: tokens.neutralOnSurface,
|
||||
),
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
color: surfaces.popover,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: Elevation.high,
|
||||
shadowColor: shadows.shadowLg.first.color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: shapes.large,
|
||||
side: BorderSide(
|
||||
color: surfaces.border.withValues(alpha: 0.15),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
textStyle: textTheme.bodyMedium?.copyWith(
|
||||
color: tokens.neutralOnSurface,
|
||||
),
|
||||
labelTextStyle: WidgetStateProperty.all(
|
||||
textTheme.bodyMedium?.copyWith(
|
||||
color: tokens.neutralOnSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
textTheme: textTheme,
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
tokens,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:conduit/core/providers/app_providers.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
@@ -8,9 +8,18 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:super_context_menu/super_context_menu.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:super_context_menu/src/scaffold/mobile/menu_widget_builder.dart'
|
||||
as mobile;
|
||||
|
||||
import 'package:conduit/features/chat/providers/chat_providers.dart' as chat;
|
||||
|
||||
/// Re-export super_context_menu types for convenience.
|
||||
export 'package:super_context_menu/super_context_menu.dart'
|
||||
show ContextMenuWidget, Menu, MenuAction, MenuSeparator;
|
||||
|
||||
/// Defines an action for use in Conduit context menus.
|
||||
class ConduitContextMenuAction {
|
||||
final IconData cupertinoIcon;
|
||||
final IconData materialIcon;
|
||||
@@ -29,89 +38,326 @@ class ConduitContextMenuAction {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> showConduitContextMenu({
|
||||
required BuildContext context,
|
||||
required List<ConduitContextMenuAction> actions,
|
||||
Offset? position,
|
||||
}) async {
|
||||
if (actions.isEmpty) return;
|
||||
/// A context menu widget that provides native iOS appearance and a beautiful
|
||||
/// Material 3 styled menu on Android.
|
||||
///
|
||||
/// On iOS, this uses the native context menu provided by super_context_menu.
|
||||
/// On Android, it displays a custom Material 3 styled menu that matches the
|
||||
/// app's theme.
|
||||
class ConduitContextMenu extends StatelessWidget {
|
||||
final List<ConduitContextMenuAction> actions;
|
||||
final Widget child;
|
||||
|
||||
final theme = context.conduitTheme;
|
||||
final RenderBox? overlay =
|
||||
Overlay.of(context).context.findRenderObject() as RenderBox?;
|
||||
const ConduitContextMenu({
|
||||
super.key,
|
||||
required this.actions,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
if (overlay == null) return;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// iOS: Use native context menu
|
||||
if (Platform.isIOS) {
|
||||
return ContextMenuWidget(
|
||||
menuProvider: (_) => buildConduitMenu(actions),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
// Determine menu position
|
||||
final Offset menuPosition = position ?? _getDefaultMenuPosition(context);
|
||||
// Android: Use ContextMenuWidget with custom Material 3 styling
|
||||
return ContextMenuWidget(
|
||||
menuProvider: (_) => buildConduitMenu(actions),
|
||||
mobileMenuWidgetBuilder: _ConduitMobileMenuBuilder(
|
||||
theme: context.conduitTheme,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final result = await showMenu<ConduitContextMenuAction>(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
menuPosition.dx,
|
||||
menuPosition.dy,
|
||||
overlay.size.width - menuPosition.dx,
|
||||
overlay.size.height - menuPosition.dy,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
||||
),
|
||||
color: theme.surfaceBackground,
|
||||
elevation: 4,
|
||||
items: actions.map((action) {
|
||||
return PopupMenuItem<ConduitContextMenuAction>(
|
||||
value: action,
|
||||
/// Custom Material 3 styled menu builder for super_context_menu on Android.
|
||||
class _ConduitMobileMenuBuilder extends mobile.MobileMenuWidgetBuilder {
|
||||
final ConduitThemeExtension theme;
|
||||
|
||||
const _ConduitMobileMenuBuilder({required this.theme});
|
||||
|
||||
@override
|
||||
Widget buildMenuContainer(
|
||||
BuildContext context,
|
||||
mobile.MobileMenuInfo menuInfo,
|
||||
Widget child,
|
||||
) {
|
||||
// Use pre-blended shadow color for Impeller compatibility
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
||||
boxShadow: theme.popoverShadows,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMenuContainerInner(
|
||||
BuildContext context,
|
||||
mobile.MobileMenuInfo menuInfo,
|
||||
Widget child,
|
||||
) {
|
||||
// Use pre-blended border color for Impeller compatibility
|
||||
final borderColor = Color.lerp(
|
||||
theme.surfaces.popover,
|
||||
theme.surfaces.border,
|
||||
0.15,
|
||||
)!;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaces.popover,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
||||
border: Border.all(color: borderColor, width: 0.5),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMenu(
|
||||
BuildContext context,
|
||||
mobile.MobileMenuInfo menuInfo,
|
||||
Widget child,
|
||||
) {
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMenuItemsContainer(
|
||||
BuildContext context,
|
||||
mobile.MobileMenuInfo menuInfo,
|
||||
Widget child,
|
||||
) {
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMenuHeader(
|
||||
BuildContext context,
|
||||
mobile.MobileMenuInfo menuInfo,
|
||||
mobile.MobileMenuButtonState state,
|
||||
) {
|
||||
// No header needed for simple menus
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildInactiveMenuVeil(
|
||||
BuildContext context,
|
||||
mobile.MobileMenuInfo menuInfo,
|
||||
) {
|
||||
// Use pre-blended solid color for Impeller compatibility
|
||||
final veilColor = theme.isDark
|
||||
? const Color(0x4D000000) // ~30% black
|
||||
: const Color(0x4D424242); // ~30% grey
|
||||
return SizedBox.expand(
|
||||
child: ColoredBox(color: veilColor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMenuItem(
|
||||
BuildContext context,
|
||||
mobile.MobileMenuInfo menuInfo,
|
||||
mobile.MobileMenuButtonState state,
|
||||
MenuElement element,
|
||||
) {
|
||||
if (element is MenuAction) {
|
||||
final isDestructive = element.attributes.destructive;
|
||||
final textColor = isDestructive ? theme.error : theme.textPrimary;
|
||||
final iconColor = isDestructive ? theme.error : theme.iconPrimary;
|
||||
final imageWidget = element.image?.asWidget(menuInfo.iconTheme);
|
||||
|
||||
// Use ColoredBox for pressed state to avoid Impeller opacity issues
|
||||
Widget content = Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.sm,
|
||||
vertical: Spacing.xxs,
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.sm + 2,
|
||||
),
|
||||
height: 36,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isIOS ? action.cupertinoIcon : action.materialIcon,
|
||||
color: action.destructive ? Colors.red : theme.iconPrimary,
|
||||
size: IconSize.xs,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
if (imageWidget != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: Spacing.md),
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: iconColor,
|
||||
size: IconSize.medium,
|
||||
),
|
||||
child: imageWidget,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
action.label,
|
||||
style: AppTypography.standard.copyWith(
|
||||
color: action.destructive ? Colors.red : theme.textPrimary,
|
||||
element.title ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: AppTypography.bodyMedium,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
color: textColor,
|
||||
decoration: TextDecoration.none,
|
||||
fontFamily: theme.typography.primaryFont.isEmpty
|
||||
? null
|
||||
: theme.typography.primaryFont,
|
||||
fontFamilyFallback: theme.typography.primaryFallback.isEmpty
|
||||
? null
|
||||
: theme.typography.primaryFallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (state.pressed) {
|
||||
content = ColoredBox(
|
||||
color: theme.surfaceContainer,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
if (element is MenuSeparator) {
|
||||
// Use pre-blended color for Impeller compatibility
|
||||
final separatorColor = Color.lerp(
|
||||
theme.surfaces.popover,
|
||||
theme.dividerColor,
|
||||
0.4,
|
||||
)!;
|
||||
return Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
indent: Spacing.md,
|
||||
endIndent: Spacing.md,
|
||||
color: separatorColor,
|
||||
);
|
||||
}
|
||||
|
||||
// For submenus or other elements, show a simple row
|
||||
if (element is Menu) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.sm + 2,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
element.title ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: AppTypography.bodyMedium,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.textPrimary,
|
||||
decoration: TextDecoration.none,
|
||||
fontFamily: theme.typography.primaryFont.isEmpty
|
||||
? null
|
||||
: theme.typography.primaryFont,
|
||||
fontFamilyFallback: theme.typography.primaryFallback.isEmpty
|
||||
? null
|
||||
: theme.typography.primaryFallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: IconSize.small,
|
||||
color: theme.iconSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildOverlayBackground(BuildContext context, double opacity) {
|
||||
// Use pre-computed hex colors for Impeller compatibility
|
||||
// These are solid colors at different opacities (0x80 = 50%, 0x66 = 40%)
|
||||
final overlayColor = Color.lerp(
|
||||
const Color(0x00000000),
|
||||
theme.isDark ? const Color(0x80000000) : const Color(0x66000000),
|
||||
opacity,
|
||||
)!;
|
||||
// GestureDetector with opaque behavior ensures hit testing works
|
||||
// even when the overlay is visually transparent
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox.expand(
|
||||
child: ColoredBox(color: overlayColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMenuPreviewContainer(BuildContext context, Widget child) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
boxShadow: theme.popoverShadows,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a [Menu] from a list of [ConduitContextMenuAction]s.
|
||||
///
|
||||
/// Use this with [ContextMenuWidget.menuProvider]:
|
||||
/// ```dart
|
||||
/// ContextMenuWidget(
|
||||
/// menuProvider: (_) => buildConduitMenu(actions),
|
||||
/// child: MyWidget(),
|
||||
/// )
|
||||
/// ```
|
||||
Menu buildConduitMenu(List<ConduitContextMenuAction> actions) {
|
||||
return Menu(
|
||||
children: actions.map((action) {
|
||||
return MenuAction(
|
||||
title: action.label,
|
||||
callback: () {
|
||||
HapticFeedback.selectionClick();
|
||||
action.onBeforeClose?.call();
|
||||
action.onSelected();
|
||||
},
|
||||
attributes: MenuActionAttributes(destructive: action.destructive),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
result.onBeforeClose?.call();
|
||||
await Future.microtask(result.onSelected);
|
||||
}
|
||||
}
|
||||
|
||||
Offset _getDefaultMenuPosition(BuildContext context) {
|
||||
final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) {
|
||||
return Offset.zero;
|
||||
}
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
final size = renderBox.size;
|
||||
return Offset(position.dx + size.width, position.dy);
|
||||
}
|
||||
|
||||
Future<void> showConversationContextMenu({
|
||||
/// Builds a list of actions for conversation context menus.
|
||||
///
|
||||
/// Use with [ConduitContextMenu]:
|
||||
/// ```dart
|
||||
/// ConduitContextMenu(
|
||||
/// actions: buildConversationActions(context: context, ref: ref, conversation: conv),
|
||||
/// child: MyWidget(),
|
||||
/// )
|
||||
/// ```
|
||||
List<ConduitContextMenuAction> buildConversationActions({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required dynamic conversation,
|
||||
}) async {
|
||||
if (conversation == null) return;
|
||||
}) {
|
||||
if (conversation == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final bool isPinned = conversation.pinned == true;
|
||||
@@ -150,48 +396,58 @@ Future<void> showConversationContextMenu({
|
||||
await _confirmAndDeleteConversation(context, ref, conversation.id);
|
||||
}
|
||||
|
||||
HapticFeedback.selectionClick();
|
||||
await showConduitContextMenu(
|
||||
context: context,
|
||||
actions: [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: isPinned
|
||||
? CupertinoIcons.pin_slash
|
||||
: CupertinoIcons.pin_fill,
|
||||
materialIcon: isPinned
|
||||
? Icons.push_pin_outlined
|
||||
: Icons.push_pin_rounded,
|
||||
label: isPinned ? l10n.unpin : l10n.pin,
|
||||
onBeforeClose: () => HapticFeedback.lightImpact(),
|
||||
onSelected: togglePin,
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: isArchived
|
||||
? CupertinoIcons.archivebox_fill
|
||||
: CupertinoIcons.archivebox,
|
||||
materialIcon: isArchived
|
||||
? Icons.unarchive_rounded
|
||||
: Icons.archive_rounded,
|
||||
label: isArchived ? l10n.unarchive : l10n.archive,
|
||||
onBeforeClose: () => HapticFeedback.lightImpact(),
|
||||
onSelected: toggleArchive,
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_rounded,
|
||||
label: l10n.rename,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: rename,
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.delete,
|
||||
materialIcon: Icons.delete_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onBeforeClose: () => HapticFeedback.mediumImpact(),
|
||||
onSelected: deleteConversation,
|
||||
),
|
||||
],
|
||||
return [
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon:
|
||||
isPinned ? CupertinoIcons.pin_slash : CupertinoIcons.pin_fill,
|
||||
materialIcon:
|
||||
isPinned ? Icons.push_pin_outlined : Icons.push_pin_rounded,
|
||||
label: isPinned ? l10n.unpin : l10n.pin,
|
||||
onBeforeClose: () => HapticFeedback.lightImpact(),
|
||||
onSelected: togglePin,
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: isArchived
|
||||
? CupertinoIcons.archivebox_fill
|
||||
: CupertinoIcons.archivebox,
|
||||
materialIcon:
|
||||
isArchived ? Icons.unarchive_rounded : Icons.archive_rounded,
|
||||
label: isArchived ? l10n.unarchive : l10n.archive,
|
||||
onBeforeClose: () => HapticFeedback.lightImpact(),
|
||||
onSelected: toggleArchive,
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.pencil,
|
||||
materialIcon: Icons.edit_rounded,
|
||||
label: l10n.rename,
|
||||
onBeforeClose: () => HapticFeedback.selectionClick(),
|
||||
onSelected: rename,
|
||||
),
|
||||
ConduitContextMenuAction(
|
||||
cupertinoIcon: CupertinoIcons.delete,
|
||||
materialIcon: Icons.delete_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onBeforeClose: () => HapticFeedback.mediumImpact(),
|
||||
onSelected: deleteConversation,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Builds a [Menu] for conversation context actions.
|
||||
///
|
||||
/// Use with [ContextMenuWidget.menuProvider].
|
||||
Menu buildConversationMenu({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required dynamic conversation,
|
||||
}) {
|
||||
return buildConduitMenu(
|
||||
buildConversationActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
conversation: conversation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,9 +477,7 @@ Future<void> _renameConversation(
|
||||
if (api == null) throw Exception('No API service');
|
||||
await api.updateConversation(conversationId, title: newName);
|
||||
HapticFeedback.selectionClick();
|
||||
ref
|
||||
.read(conversationsProvider.notifier)
|
||||
.updateConversation(
|
||||
ref.read(conversationsProvider.notifier).updateConversation(
|
||||
conversationId,
|
||||
(conversation) =>
|
||||
conversation.copyWith(title: newName, updatedAt: DateTime.now()),
|
||||
|
||||
@@ -164,26 +164,6 @@ class IOSEnhancements {
|
||||
);
|
||||
}
|
||||
|
||||
/// Create iOS-style context menu
|
||||
static Widget createContextMenu({
|
||||
required Widget child,
|
||||
required List<ContextMenuAction> actions,
|
||||
}) {
|
||||
return CupertinoContextMenu(
|
||||
actions: actions
|
||||
.map(
|
||||
(action) => CupertinoContextMenuAction(
|
||||
onPressed: action.onPressed,
|
||||
isDefaultAction: action.isDefault,
|
||||
isDestructiveAction: action.isDestructive,
|
||||
child: Text(action.title),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create iOS-style action sheet
|
||||
static void showActionSheet({
|
||||
required BuildContext context,
|
||||
@@ -459,20 +439,6 @@ enum ButtonType { filled, outlined, text }
|
||||
|
||||
enum CardType { filled, outlined, elevated }
|
||||
|
||||
class ContextMenuAction {
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
final bool isDefault;
|
||||
final bool isDestructive;
|
||||
|
||||
const ContextMenuAction({
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
this.isDefault = false,
|
||||
this.isDestructive = false,
|
||||
});
|
||||
}
|
||||
|
||||
class ActionSheetAction {
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
80
pubspec.lock
80
pubspec.lock
@@ -377,6 +377,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.5.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -885,6 +901,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
irondash_engine_context:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: irondash_engine_context
|
||||
sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.5"
|
||||
irondash_message_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: irondash_message_channel
|
||||
sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
isolate_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1149,6 +1181,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
pixel_snap:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pixel_snap
|
||||
sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1650,6 +1690,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
super_clipboard:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: super_clipboard
|
||||
sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
super_context_menu:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: super_context_menu
|
||||
sha256: "44570d342bea2381c57520f181016207c0ffde401f05f641e6dfec495fa728fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
super_drag_and_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: super_drag_and_drop
|
||||
sha256: "8946913a021cb617c35e36cfe57e8b817335643d7ee9bbc83d6e11760136bd1c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
super_native_extensions:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: super_native_extensions
|
||||
sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1938,6 +2010,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: conduit
|
||||
description: Open-source mobile client for Open-WebUI
|
||||
version: 2.3.9+86
|
||||
version: 2.3.10+87
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
@@ -80,6 +80,8 @@ dependencies:
|
||||
home_widget: ^0.8.1
|
||||
flutter_highlight: ^0.7.0
|
||||
pasteboard: ^0.4.0
|
||||
super_context_menu: ^0.9.1
|
||||
super_drag_and_drop: ^0.9.1
|
||||
|
||||
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user