System Share on iOS

This commit is contained in:
LittleSheep 2024-12-15 12:59:18 +08:00
parent 09ad917e5d
commit 89c912a35b
9 changed files with 166 additions and 46 deletions

View File

@ -49,26 +49,6 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" /> <data android:mimeType="text/*" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

View File

@ -445,5 +445,8 @@
"postShare": "Share", "postShare": "Share",
"postShareImage": "Share via Image", "postShareImage": "Share via Image",
"appInitializing": "Initializing", "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"
} }

View File

@ -443,5 +443,8 @@
"postShare": "分享", "postShare": "分享",
"postShareImage": "分享帖图", "postShareImage": "分享帖图",
"appInitializing": "正在初始化", "appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持" "poweredBy": "由 {} 提供支持",
"shareIntent": "分享",
"shareIntentDescription": "您想对您分享的内容做些什么?",
"shareIntentPostStory": "发布动态"
} }

View File

@ -592,7 +592,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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 */ = { 43B5CF57FD79BC21654EE037 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;

View File

@ -11,14 +11,14 @@
<dict> <dict>
<key>NSExtensionActivationSupportsText</key> <key>NSExtensionActivationSupportsText</key>
<true/> <true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key> <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer> <integer>1</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key> <key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>100</integer> <integer>100</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key> <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>100</integer> <integer>100</integer>
<key>NSExtensionActivationSupportsFileWithMaxCount</key> <key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>100</integer> <integer>100</integer>
</dict> </dict>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
@ -32,5 +32,7 @@
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string> <string>com.apple.share-services</string>
</dict> </dict>
<key>AppGroupId</key>
<string>group.solsynth.solian</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
@ -12,9 +11,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:home_widget/home_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.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/dialog.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.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 @override
void initState() { void initState() {
super.initState(); super.initState();
_initialize(); _initialize();
_listenShareIntent();
} }
@override @override

View File

@ -28,6 +28,7 @@ import 'package:surface/screens/realm.dart';
import 'package:surface/screens/realm/manage.dart'; import 'package:surface/screens/realm/manage.dart';
import 'package:surface/screens/realm/realm_detail.dart'; import 'package:surface/screens/realm/realm_detail.dart';
import 'package:surface/screens/settings.dart'; import 'package:surface/screens/settings.dart';
import 'package:surface/screens/sharing.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_background.dart';
@ -69,6 +70,7 @@ final _appRoutes = [
postRepostId: int.tryParse( postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '', state.uri.queryParameters['reposting'] ?? '',
), ),
extraProps: state.extra as PostEditorExtraProps?,
), ),
), ),
), ),
@ -315,7 +317,9 @@ final appRouter = GoRouter(
routes: [ routes: [
ShellRoute( ShellRoute(
routes: _appRoutes, routes: _appRoutes,
builder: (context, state, child) => AppRootScaffold(body: child), builder: (context, state, child) => AppRootScaffold(
body: AppSharingListener(child: child),
),
), ),
], ],
); );

View File

@ -23,11 +23,26 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class PostEditorExtraProps {
final String? text;
final String? title;
final String? description;
final List<PostWriteMedia>? attachments;
const PostEditorExtraProps({
this.text,
this.title,
this.description,
this.attachments,
});
}
class PostEditorScreen extends StatefulWidget { class PostEditorScreen extends StatefulWidget {
final String mode; final String mode;
final int? postEditId; final int? postEditId;
final int? postReplyId; final int? postReplyId;
final int? postRepostId; final int? postRepostId;
final PostEditorExtraProps? extraProps;
const PostEditorScreen({ const PostEditorScreen({
super.key, super.key,
@ -35,6 +50,7 @@ class PostEditorScreen extends StatefulWidget {
required this.postEditId, required this.postEditId,
required this.postReplyId, required this.postReplyId,
required this.postRepostId, required this.postRepostId,
this.extraProps,
}); });
@override @override
@ -130,6 +146,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
replying: widget.postReplyId, replying: widget.postReplyId,
reposting: widget.postRepostId, 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 @override
@ -150,15 +172,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
TextSpan( TextSpan(
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(), text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!, color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: PostWriteController.kTitleMap[widget.mode]!.tr(), text: PostWriteController.kTitleMap[widget.mode]!.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!, color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
]), ]),
), ),

115
lib/screens/sharing.dart Normal file
View File

@ -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<AppSharingListener> createState() => _AppSharingListenerState();
}
class _AppSharingListenerState extends State<AppSharingListener> {
late StreamSubscription _shareIntentSubscription;
void _gotoPost(Iterable<SharedMediaFile> 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;
}
}