From 434256e61e33d9bb46a5f29ca04238ec8556f515 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 25 Jun 2025 15:58:58 +0800 Subject: [PATCH] :sparkles: System begin share --- assets/i18n/en-US.json | 1 + lib/widgets/share/share_sheet.dart | 349 ++++++++++-------- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + pubspec.lock | 20 +- pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 219 insertions(+), 164 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 5fc779d..d986c18 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -560,5 +560,6 @@ "failedToCopy": "Failed to copy: {}", "noChatRoomsAvailable": "No chat rooms available", "failedToLoadChats": "Failed to load chats", + "contentToShare": "Content to share:", "unknownChat": "Unknown Chat" } diff --git a/lib/widgets/share/share_sheet.dart b/lib/widgets/share/share_sheet.dart index 19e69fb..036965e 100644 --- a/lib/widgets/share/share_sheet.dart +++ b/lib/widgets/share/share_sheet.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:cross_file/cross_file.dart'; + import 'dart:io'; import 'package:path/path.dart' as path; import 'package:island/models/chat.dart'; import 'package:island/screens/chat/chat.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:share_plus/share_plus.dart'; enum ShareContentType { text, link, file } @@ -125,15 +127,9 @@ class _ShareSheetState extends ConsumerState { try { // TODO: Implement share to post functionality // This would typically navigate to the post composer with pre-filled content - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Share to post functionality coming soon'), - ), - ); + showSnackBar(context, 'Share to post functionality coming soon'); } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('failedToShareToPost'.tr(args: [e.toString()])))); + showErrorAlert(e); } finally { setState(() => _isLoading = false); } @@ -141,18 +137,8 @@ class _ShareSheetState extends ConsumerState { Future _shareToChat() async { setState(() => _isLoading = true); - try { - // TODO: Implement share to chat functionality - // This would typically show a chat selection dialog - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('shareToChatComingSoon'.tr()), - ), - ); - } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('failedToShareToChat'.tr(args: [e.toString()])))); + try {} catch (e) { + showErrorAlert(e); } finally { setState(() => _isLoading = false); } @@ -161,11 +147,13 @@ class _ShareSheetState extends ConsumerState { Future _shareToSpecificChat(SnChatRoom chatRoom) async { setState(() => _isLoading = true); try { - // TODO: Implement share to specific chat functionality - // This would send the content to the selected chat room ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('shareToSpecificChatComingSoon'.tr(args: [chatRoom.name ?? 'directChat'.tr()])), + content: Text( + 'shareToSpecificChatComingSoon'.tr( + args: [chatRoom.name ?? 'directChat'.tr()], + ), + ), ), ); } catch (e) { @@ -182,17 +170,29 @@ class _ShareSheetState extends ConsumerState { setState(() => _isLoading = true); try { - // TODO: Implement system share functionality - // This would use platform-specific sharing APIs - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('systemShareComingSoon'.tr())), - ); + switch (widget.content.type) { + case ShareContentType.text: + if (widget.content.text?.isNotEmpty == true) { + await Share.share(widget.content.text!); + } + break; + case ShareContentType.link: + if (widget.content.link?.isNotEmpty == true) { + await Share.share(widget.content.link!); + } + break; + case ShareContentType.file: + if (widget.content.files?.isNotEmpty == true) { + await Share.shareXFiles(widget.content.files!); + } + break; + } } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('failedToShareToSystem'.tr(args: [e.toString()])))); + showErrorAlert(e); } finally { - setState(() => _isLoading = false); + if (mounted) { + setState(() => _isLoading = false); + } } } @@ -213,13 +213,9 @@ class _ShareSheetState extends ConsumerState { } await Clipboard.setData(ClipboardData(text: textToCopy)); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('copyToClipboard'.tr()))); + if (mounted) showSnackBar(context, 'copyToClipboard'.tr()); } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('failedToCopy'.tr(args: [e.toString()])))); + showErrorAlert(e); } } @@ -227,6 +223,7 @@ class _ShareSheetState extends ConsumerState { Widget build(BuildContext context) { return SheetScaffold( titleText: widget.title ?? 'share'.tr(), + heightFactor: 0.75, child: Column( children: [ // Content preview @@ -241,7 +238,7 @@ class _ShareSheetState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Content to share:', + 'contentToShare'.tr(), style: Theme.of(context).textTheme.labelMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), @@ -254,77 +251,86 @@ class _ShareSheetState extends ConsumerState { // Share options Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Quick actions row (horizontally scrollable) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'quickActions'.tr(), - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quick actions row (horizontally scrollable) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'quickActions'.tr(), + style: Theme.of( + context, + ).textTheme.titleSmall?.copyWith( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), ), - ), - const SizedBox(height: 12), - SizedBox( - height: 80, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - _CompactShareOption( - icon: Symbols.post_add, - title: 'post'.tr(), - onTap: _isLoading ? null : _shareToPost, - ), - const SizedBox(width: 12), - _CompactShareOption( - icon: Symbols.content_copy, - title: 'copy'.tr(), - onTap: _isLoading ? null : _copyToClipboard, - ), - if (widget.toSystem) ...[ + const SizedBox(height: 12), + SizedBox( + height: 80, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + _CompactShareOption( + icon: Symbols.post_add, + title: 'post'.tr(), + onTap: _isLoading ? null : _shareToPost, + ), const SizedBox(width: 12), _CompactShareOption( - icon: Symbols.share, - title: 'share'.tr(), - onTap: _isLoading ? null : _shareToSystem, + icon: Symbols.content_copy, + title: 'copy'.tr(), + onTap: _isLoading ? null : _copyToClipboard, ), + if (widget.toSystem) ...[ + const SizedBox(width: 12), + _CompactShareOption( + icon: Symbols.share, + title: 'share'.tr(), + onTap: _isLoading ? null : _shareToSystem, + ), + ], ], - ], + ), ), - ), - ], + ], + ), ), - ), - - const SizedBox(height: 24), - - // Chat section - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'sendToChat'.tr(), - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + + const SizedBox(height: 24), + + // Chat section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'sendToChat'.tr(), + style: Theme.of( + context, + ).textTheme.titleSmall?.copyWith( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), ), - ), - const SizedBox(height: 12), - _ChatRoomsList( - onChatSelected: _isLoading ? null : _shareToSpecificChat, - ), - ], + const SizedBox(height: 12), + _ChatRoomsList( + onChatSelected: + _isLoading ? null : _shareToSpecificChat, + ), + ], + ), ), - ), - - const SizedBox(height: 16), - ], + + const SizedBox(height: 16), + ], + ), ), ), @@ -382,36 +388,39 @@ class _ChatRoomsList extends ConsumerWidget { final room = rooms[index]; return _ChatRoomOption( room: room, - onTap: onChatSelected != null ? () => onChatSelected!(room) : null, + onTap: + onChatSelected != null ? () => onChatSelected!(room) : null, ); }, ), ); }, - loading: () => SizedBox( - height: 80, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - error: (error, stack) => Container( - height: 80, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.errorContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Center( - child: Text( - 'failedToLoadChats'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onErrorContainer, + loading: + () => SizedBox( + height: 80, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + error: + (error, stack) => Container( + height: 80, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + 'failedToLoadChats'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), ), ), - ), - ), ); } } @@ -420,16 +429,14 @@ class _ChatRoomOption extends StatelessWidget { final SnChatRoom room; final VoidCallback? onTap; - const _ChatRoomOption({ - required this.room, - this.onTap, - }); + const _ChatRoomOption({required this.room, this.onTap}); @override Widget build(BuildContext context) { final isDirect = room.type == 1; // Assuming type 1 is direct chat - final displayName = room.name ?? - (isDirect && room.members != null + final displayName = + room.name ?? + (isDirect && room.members != null ? room.members!.map((m) => m.account.nick).join(', ') : 'unknownChat'.tr()); @@ -439,9 +446,12 @@ class _ChatRoomOption extends StatelessWidget { width: 72, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: onTap != null - ? Theme.of(context).colorScheme.surfaceContainerHighest - : Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5), + color: + onTap != null + ? Theme.of(context).colorScheme.surfaceContainerHighest + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withOpacity(0.5), borderRadius: BorderRadius.circular(12), border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), @@ -458,28 +468,32 @@ class _ChatRoomOption extends StatelessWidget { color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(16), ), - child: room.picture != null - ? ClipRRect( - borderRadius: BorderRadius.circular(16), - child: CloudFileWidget( - item: room.picture!, - fit: BoxFit.cover, - ), - ) - : Icon( - isDirect ? Symbols.person : Symbols.group, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), + child: + room.picture != null + ? ClipRRect( + borderRadius: BorderRadius.circular(16), + child: CloudFileWidget( + item: room.picture!, + fit: BoxFit.cover, + ), + ) + : Icon( + isDirect ? Symbols.person : Symbols.group, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), ), const SizedBox(height: 4), // Chat room name Text( displayName, style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: onTap != null - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5), + color: + onTap != null + ? Theme.of(context).colorScheme.onSurface + : Theme.of( + context, + ).colorScheme.onSurfaceVariant.withOpacity(0.5), ), textAlign: TextAlign.center, maxLines: 1, @@ -511,9 +525,12 @@ class _CompactShareOption extends StatelessWidget { width: 72, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: onTap != null - ? Theme.of(context).colorScheme.surfaceContainerHighest - : Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5), + color: + onTap != null + ? Theme.of(context).colorScheme.surfaceContainerHighest + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withOpacity(0.5), borderRadius: BorderRadius.circular(12), border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), @@ -525,17 +542,23 @@ class _CompactShareOption extends StatelessWidget { Icon( icon, size: 24, - color: onTap != null - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5), + color: + onTap != null + ? Theme.of(context).colorScheme.primary + : Theme.of( + context, + ).colorScheme.onSurfaceVariant.withOpacity(0.5), ), const SizedBox(height: 4), Text( title, style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: onTap != null - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5), + color: + onTap != null + ? Theme.of(context).colorScheme.onSurface + : Theme.of( + context, + ).colorScheme.onSurfaceVariant.withOpacity(0.5), ), textAlign: TextAlign.center, maxLines: 1, @@ -623,6 +646,8 @@ class _ContentPreview extends StatelessWidget { } } +const double kPreviewMaxHeight = 80; + class _TextPreview extends StatelessWidget { final String text; @@ -631,7 +656,7 @@ class _TextPreview extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - constraints: const BoxConstraints(maxHeight: 120), + constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight), child: SingleChildScrollView( child: Text(text, style: Theme.of(context).textTheme.bodyMedium), ), @@ -647,7 +672,7 @@ class _LinkPreview extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - constraints: const BoxConstraints(maxHeight: 120), + constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -730,7 +755,7 @@ class _FilePreview extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - constraints: const BoxConstraints(maxHeight: 200), + constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 191be60..ec1d980 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -27,6 +27,7 @@ import package_info_plus import pasteboard import path_provider_foundation import record_macos +import share_plus import shared_preferences_foundation import sign_in_with_apple import sqflite_darwin @@ -59,6 +60,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7f068ba..69bbdff 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -118,6 +118,8 @@ PODS: - record_macos (1.0.0): - FlutterMacOS - SAMKeychain (1.5.3) + - share_plus (0.0.1): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -183,6 +185,7 @@ DEPENDENCIES: - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) @@ -257,6 +260,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin record_macos: :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sign_in_with_apple: @@ -310,6 +315,7 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 diff --git a/pubspec.lock b/pubspec.lock index e35b420..14e0c8b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1841,10 +1841,10 @@ packages: dependency: transitive description: name: screen_brightness_android - sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e" + sha256: fb5fa43cb89d0c9b8534556c427db1e97e46594ac5d66ebdcf16063b773d54ed url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" screen_brightness_platform_interface: dependency: transitive description: @@ -1869,6 +1869,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.2" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 + url: "https://pub.dev" + source: hosted + version: "11.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" + url: "https://pub.dev" + source: hosted + version: "6.0.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c6f1e67..c180af8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -119,6 +119,7 @@ dependencies: local_auth: ^2.3.0 flutter_secure_storage: ^4.2.1 flutter_math_fork: ^0.7.4 + share_plus: ^11.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d6d49aa..0208ed1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PasteboardPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); SuperNativeExtensionsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 93d5067..b02a140 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -20,6 +20,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_video pasteboard record_windows + share_plus sqlite3_flutter_libs super_native_extensions url_launcher_windows