From 97ace86b12f786d399dce1f27facff3a3a2f9175 Mon Sep 17 00:00:00 2001 From: cogwheel <172976095+cogwheel0@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:26:03 +0530 Subject: [PATCH] feat(ui): Refactor context menu with platform-specific styling feat(navigation): migrate to super_drag_and_drop for folder drag and drop feat(ui): Add context menu preview builders for chat and notes refactor(ui): Remove preview builders and simplify note card rendering --- ios/Podfile | 9 +- ios/Podfile.lock | 28 +- ios/Runner.xcodeproj/project.pbxproj | 226 +++---- lib/features/chat/views/chat_page.dart | 21 +- .../chat/widgets/user_message_bubble.dart | 70 +-- .../navigation/widgets/chats_drawer.dart | 566 +++++++++--------- .../notes/views/note_editor_page.dart | 66 +- lib/features/notes/views/notes_list_page.dart | 166 +++-- lib/shared/theme/app_theme.dart | 22 + .../utils/conversation_context_menu.dart | 464 ++++++++++---- lib/shared/utils/platform_utils.dart | 34 -- pubspec.lock | 80 +++ pubspec.yaml | 4 +- 13 files changed, 1032 insertions(+), 724 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 802bd24..96c365d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3525633..126e4a2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a8c5b70..5fe47e5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; + 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 = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; - 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; - 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 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 = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; @@ -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 = ""; @@ -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)"; diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 9bbf85f..23381ab 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1817,18 +1817,15 @@ class _ChatPageState extends ConsumerState { ), ); } 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( diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 01aa0b8..8750cf5 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -433,51 +433,32 @@ class _UserMessageBubbleState extends ConsumerState { super.dispose(); } - Future _showMessageMenu(BuildContext context) async { - // Don't show menu while editing - use the visible Save/Cancel buttons instead - if (_isEditing) return; + List _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 @@ -505,9 +486,8 @@ class _UserMessageBubbleState extends ConsumerState { 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( diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 69e0e32..fe83dd4 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -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 { 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 { ), 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 { 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 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 { } } }, - 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 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 { 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 { 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 { ), ), ), - ); - }, + ), + ), ); } @@ -1296,37 +1302,33 @@ class _ChatsDrawerState extends ConsumerState { ); } - void _showFolderContextMenu( - BuildContext context, + List _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 { 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 { } } }, - 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 { (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 { 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> { void set(Map value) => state = Map.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 = []; + final trailingWidgets = []; + 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 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 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, + ), ), ), ), diff --git a/lib/features/notes/views/note_editor_page.dart b/lib/features/notes/views/note_editor_page.dart index ce847c0..f95e3e0 100644 --- a/lib/features/notes/views/note_editor_page.dart +++ b/lib/features/notes/views/note_editor_page.dart @@ -709,39 +709,50 @@ class _NoteEditorPageState extends ConsumerState { ), ), ), - // Actions (more menu) + // Actions (more menu) - uses PopupMenuButton for tap interaction Padding( padding: const EdgeInsets.only(right: Spacing.inputPadding), child: Center( child: PopupMenuButton( - 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 { 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 { 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 { } 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 { 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 { 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, ), ), diff --git a/lib/features/notes/views/notes_list_page.dart b/lib/features/notes/views/notes_list_page.dart index c6ac950..c974a48 100644 --- a/lib/features/notes/views/notes_list_page.dart +++ b/lib/features/notes/views/notes_list_page.dart @@ -414,33 +414,40 @@ class _NotesListPageState extends ConsumerState { 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 { 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 { ], ), ), - // 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 { date.day == now.day; } - void _showNoteContextMenu(BuildContext context, Note note) { + List _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) { diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index ccd8877..3747e01 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -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: >[ tokens, diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index 45b1a24..f597e8b 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -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 showConduitContextMenu({ - required BuildContext context, - required List 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 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( - 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( - 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 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 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 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 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 _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()), diff --git a/lib/shared/utils/platform_utils.dart b/lib/shared/utils/platform_utils.dart index 534096d..63106af 100644 --- a/lib/shared/utils/platform_utils.dart +++ b/lib/shared/utils/platform_utils.dart @@ -164,26 +164,6 @@ class IOSEnhancements { ); } - /// Create iOS-style context menu - static Widget createContextMenu({ - required Widget child, - required List 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; diff --git a/pubspec.lock b/pubspec.lock index 06583da..3424fc9 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 0b01af5..097490f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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)