From 89c912a35b0bb24722134d720f62bda13f2ca06f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 15 Dec 2024 12:59:18 +0800 Subject: [PATCH] :sparkles: System Share on iOS --- android/app/src/main/AndroidManifest.xml | 20 ---- assets/translations/en-US.json | 5 +- assets/translations/zh-CN.json | 5 +- ios/Runner.xcodeproj/project.pbxproj | 2 +- ios/SolarShare/Info.plist | 18 ++-- lib/main.dart | 11 +-- lib/router.dart | 6 +- lib/screens/post/post_editor.dart | 30 +++++- lib/screens/sharing.dart | 115 +++++++++++++++++++++++ 9 files changed, 166 insertions(+), 46 deletions(-) create mode 100644 lib/screens/sharing.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2c92c16..4f61729 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,26 +49,6 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 25a4d52..7c9194a 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -445,5 +445,8 @@ "postShare": "Share", "postShareImage": "Share via Image", "appInitializing": "Initializing", - "poweredBy": "Powered by {}" + "poweredBy": "Powered by {}", + "shareIntent": "Share", + "shareIntentDescription": "What do you want to do with the content you are sharing?", + "shareIntentPostStory": "Post a Story" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index c29a278..4b486c6 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -443,5 +443,8 @@ "postShare": "分享", "postShareImage": "分享帖图", "appInitializing": "正在初始化", - "poweredBy": "由 {} 提供支持" + "poweredBy": "由 {} 提供支持", + "shareIntent": "分享", + "shareIntentDescription": "您想对您分享的内容做些什么?", + "shareIntentPostStory": "发布动态" } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2197bc3..60489d0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -592,7 +592,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 43B5CF57FD79BC21654EE037 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; diff --git a/ios/SolarShare/Info.plist b/ios/SolarShare/Info.plist index b256dc6..97566e8 100644 --- a/ios/SolarShare/Info.plist +++ b/ios/SolarShare/Info.plist @@ -11,14 +11,14 @@ NSExtensionActivationSupportsText - NSExtensionActivationSupportsWebURLWithMaxCount - 1 - NSExtensionActivationSupportsImageWithMaxCount - 100 - NSExtensionActivationSupportsMovieWithMaxCount - 100 - NSExtensionActivationSupportsFileWithMaxCount - 100 + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 100 + NSExtensionActivationSupportsMovieWithMaxCount + 100 + NSExtensionActivationSupportsFileWithMaxCount + 100 NSExtension @@ -32,5 +32,7 @@ NSExtensionPointIdentifier com.apple.share-services + AppGroupId + group.solsynth.solian diff --git a/lib/main.dart b/lib/main.dart index f4d8184..bad11c0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; @@ -12,9 +11,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:home_widget/home_widget.dart'; import 'package:provider/provider.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:relative_time/relative_time.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -40,6 +37,7 @@ import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/version_label.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); @@ -191,17 +189,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { } } - void _listenShareIntent() async { - _shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) {}, onError: (err) { - log("[ShareIntent] Unable to subscribe: $err"); - }); - } - @override void initState() { super.initState(); _initialize(); - _listenShareIntent(); } @override diff --git a/lib/router.dart b/lib/router.dart index 25a0b18..8047a41 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -28,6 +28,7 @@ import 'package:surface/screens/realm.dart'; import 'package:surface/screens/realm/manage.dart'; import 'package:surface/screens/realm/realm_detail.dart'; import 'package:surface/screens/settings.dart'; +import 'package:surface/screens/sharing.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/navigation/app_background.dart'; @@ -69,6 +70,7 @@ final _appRoutes = [ postRepostId: int.tryParse( state.uri.queryParameters['reposting'] ?? '', ), + extraProps: state.extra as PostEditorExtraProps?, ), ), ), @@ -315,7 +317,9 @@ final appRouter = GoRouter( routes: [ ShellRoute( routes: _appRoutes, - builder: (context, state, child) => AppRootScaffold(body: child), + builder: (context, state, child) => AppRootScaffold( + body: AppSharingListener(child: child), + ), ), ], ); diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 60c0a33..c11c508 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -23,11 +23,26 @@ import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:provider/provider.dart'; +class PostEditorExtraProps { + final String? text; + final String? title; + final String? description; + final List? attachments; + + const PostEditorExtraProps({ + this.text, + this.title, + this.description, + this.attachments, + }); +} + class PostEditorScreen extends StatefulWidget { final String mode; final int? postEditId; final int? postReplyId; final int? postRepostId; + final PostEditorExtraProps? extraProps; const PostEditorScreen({ super.key, @@ -35,6 +50,7 @@ class PostEditorScreen extends StatefulWidget { required this.postEditId, required this.postReplyId, required this.postRepostId, + this.extraProps, }); @override @@ -130,6 +146,12 @@ class _PostEditorScreenState extends State { replying: widget.postReplyId, reposting: widget.postRepostId, ); + if (widget.extraProps != null) { + _writeController.contentController.text = widget.extraProps!.text ?? ''; + _writeController.titleController.text = widget.extraProps!.title ?? ''; + _writeController.descriptionController.text = widget.extraProps!.description ?? ''; + _writeController.addAttachments(widget.extraProps!.attachments ?? []); + } } @override @@ -150,15 +172,15 @@ class _PostEditorScreenState extends State { TextSpan( text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(), style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).appBarTheme.foregroundColor!, - ), + color: Theme.of(context).appBarTheme.foregroundColor!, + ), ), const TextSpan(text: '\n'), TextSpan( text: PostWriteController.kTitleMap[widget.mode]!.tr(), style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: Theme.of(context).appBarTheme.foregroundColor!, - ), + color: Theme.of(context).appBarTheme.foregroundColor!, + ), ), ]), ), diff --git a/lib/screens/sharing.dart b/lib/screens/sharing.dart new file mode 100644 index 0000000..a699bc5 --- /dev/null +++ b/lib/screens/sharing.dart @@ -0,0 +1,115 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:cross_file/cross_file.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:surface/controllers/post_write_controller.dart'; +import 'package:surface/screens/post/post_editor.dart'; +import 'package:surface/widgets/dialog.dart'; + +class AppSharingListener extends StatefulWidget { + final Widget child; + + const AppSharingListener({super.key, required this.child}); + + @override + State createState() => _AppSharingListenerState(); +} + +class _AppSharingListenerState extends State { + late StreamSubscription _shareIntentSubscription; + + void _gotoPost(Iterable value) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('shareIntent').tr(), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('shareIntentDescription').tr(), + const Gap(8), + Card( + child: Column( + children: [ + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + leading: Icon(Icons.post_add), + trailing: const Icon(Icons.chevron_right), + title: Text('shareIntentPostStory').tr(), + onTap: () { + GoRouter.of(context).pushNamed( + 'postEditor', + pathParameters: { + 'mode': 'stories', + }, + extra: PostEditorExtraProps( + attachments: value.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(), + ), + ); + Navigator.pop(context); + }, + ), + ], + ), + ) + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('dialogDismiss').tr(), + ) + ], + ), + ); + }); + } + + void _initialize() async { + _shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) { + if (value.isEmpty) return; + if (mounted) { + _gotoPost(value); + } + }, onError: (err) { + log("[ShareIntent] Unable to subscribe: $err"); + }); + } + + void _initialHandle() { + ReceiveSharingIntent.instance.getInitialMedia().then((value) { + if (value.isEmpty) return; + if (mounted) { + _gotoPost(value); + } + ReceiveSharingIntent.instance.reset(); + }); + } + + @override + void initState() { + super.initState(); + _initialize(); + _initialHandle(); + } + + @override + void dispose() { + _shareIntentSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +}