implemented native share intent support for android and ios with chat and folder pickers

This commit is contained in:
2026-05-09 19:42:51 +02:00
parent 00664c66a8
commit cb2c38aaa1
25 changed files with 1046 additions and 26 deletions
+15
View File
@@ -0,0 +1,15 @@
class PendingShare {
final List<String> filePaths;
final String? text;
final DateTime receivedAt;
const PendingShare({
required this.filePaths,
required this.text,
required this.receivedAt,
});
bool get hasFiles => filePaths.isNotEmpty;
bool get hasText => text != null && text!.isNotEmpty;
bool get isEmpty => !hasFiles && !hasText;
}
@@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'pending_share.dart';
/// Bridges native share intents (Android ACTION_SEND, iOS Share Extension)
/// into a single [ValueNotifier] that the app routes off of.
class ShareIntentListener {
ShareIntentListener._();
static final ShareIntentListener instance = ShareIntentListener._();
static final ValueNotifier<PendingShare?> pending = ValueNotifier(null);
StreamSubscription<List<SharedMediaFile>>? _streamSub;
bool _initialized = false;
/// Reads the cold-start payload exactly once. Call from `main()` before
/// `runApp` so the share is queued before the UI mounts.
Future<void> initialize() async {
if (_initialized) return;
_initialized = true;
try {
final initial = await ReceiveSharingIntent.instance.getInitialMedia();
final share = _toPendingShare(initial);
if (share != null) pending.value = share;
await ReceiveSharingIntent.instance.reset();
} catch (e) {
debugPrint('ShareIntentListener.initialize failed: $e');
}
}
/// Subscribes to warm-share stream events. Safe to call multiple times.
void attach() {
_streamSub ??= ReceiveSharingIntent.instance.getMediaStream().listen(
(items) {
final share = _toPendingShare(items);
if (share != null) pending.value = share;
},
onError: (Object e) =>
debugPrint('ShareIntentListener stream error: $e'),
);
}
/// Cancels the warm-share subscription. The singleton survives, so a
/// subsequent [attach] re-subscribes.
void detach() {
_streamSub?.cancel();
_streamSub = null;
}
/// Discards the current share and removes any temp files the plugin copied
/// into the app cache. Idempotent.
void clear() {
final current = pending.value;
pending.value = null;
if (current != null) {
for (final path in current.filePaths) {
try {
final f = File(path);
if (f.existsSync()) f.deleteSync();
} catch (_) {
// best-effort cleanup; OS will reclaim cache eventually
}
}
}
unawaited(ReceiveSharingIntent.instance.reset());
}
PendingShare? _toPendingShare(List<SharedMediaFile> items) {
if (items.isEmpty) return null;
final files = <String>[];
final texts = <String>[];
for (final item in items) {
switch (item.type) {
case SharedMediaType.image:
case SharedMediaType.video:
case SharedMediaType.file:
files.add(item.path);
case SharedMediaType.text:
case SharedMediaType.url:
texts.add(item.path);
}
}
if (files.isEmpty && texts.isEmpty) return null;
return PendingShare(
filePaths: files,
text: texts.isEmpty ? null : texts.join('\n'),
receivedAt: DateTime.now(),
);
}
}