// global_modal.dart // 全局弹窗管理器 // 简要说明:使用overlay挂载在全局widget上,以此实现对全局的覆盖, // overlay为全局独立的一个层级,dialog等通常也是 由他实现 import 'dart:async'; import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; // ========================== // 1) 公共类型定义 // ========================== typedef ModalBuilder = Widget Function( BuildContext context, ModalRequest req, void Function(ModalResult) complete, ); enum ModalPriority { low, normal, high, critical } class ModalRequest { final String type; // 业务类型:'friend_request' | 'live_invite' | ... final Map payload;// 自定义业务数据 final String? dedupeKey; // 去重键(如 'friendReq:123') final ModalPriority priority; // 优先级 final Duration? ttl; // 过期丢弃窗口(从创建到入队/处理) final Duration? timeout; // 自动超时 final DateTime createdAt = DateTime.now(); ModalRequest({ required this.type, required this.payload, this.dedupeKey, this.priority = ModalPriority.normal, this.ttl, this.timeout, }); } class ModalResult { final String action; // 'accept' | 'decline' | 'timeout' | 'dismiss' | 'expired' | 'deduped' | 'unknown_type' final Map? data; const ModalResult(this.action, {this.data}); } // ========================== // 2) 全局控制器 + Host + 队列 // ========================== class GlobalModalController { GlobalModalController._(); static final GlobalModalController I = GlobalModalController._(); GlobalKey? _overlayKey; void setOverlayKey(GlobalKey key) { _overlayKey = key; _pump(); // key 就绪后尝试展示队列 } final Map _registry = {}; final PriorityQueue<_QueueItem> _queue = PriorityQueue( (a, b) { final prio = b.req.priority.index.compareTo(a.req.priority.index); if (prio != 0) return prio; return a.idx.compareTo(b.idx); // 同优先级按入队序 }, ); int _seq = 0; Completer? _activeCompleter; OverlayEntry? _entry; BuildContext? _hostContext; bool _isForeground = true; final Map _recentKeys = {}; void attachHost(BuildContext ctx) { _hostContext = ctx; _pump(); } void register(String type, ModalBuilder builder) { _registry[type] = builder; } Future enqueue(ModalRequest req) async { if (req.ttl != null && DateTime.now().difference(req.createdAt) > req.ttl!) { return const ModalResult('expired'); } if (req.dedupeKey != null) { final last = _recentKeys[req.dedupeKey!]; if (last != null && DateTime.now().difference(last) < const Duration(seconds: 10)) { return const ModalResult('deduped'); } _recentKeys[req.dedupeKey!] = DateTime.now(); } final c = Completer(); _queue.add(_QueueItem(idx: _seq++, req: req, completer: c)); _pump(); return c.future; } void _pump() { if (_activeCompleter != null || _overlayKey?.currentState == null || !_isForeground) return; if (_queue.isEmpty) return; final item = _queue.removeFirst(); final builder = _registry[item.req.type]; if (builder == null) { item.completer.complete(const ModalResult('unknown_type')); _pump(); return; } _activeCompleter = item.completer; void onComplete(ModalResult r) { _dismiss(); _activeCompleter?.complete(r); _activeCompleter = null; _pump(); } Timer? to; if (item.req.timeout != null) { to = Timer(item.req.timeout!, () { if (_activeCompleter != null) onComplete(const ModalResult('timeout')); }); } _entry = OverlayEntry( maintainState: true, builder: (ctx) => _ModalScope( child: builder(ctx, item.req, (r) { to?.cancel(); onComplete(r); }), onBackgroundTap: () { to?.cancel(); onComplete(const ModalResult('dismiss')); }, ), ); _overlayKey!.currentState!.insert(_entry!); } void _dismiss() { // ✅ 防御性处理:只有在 entry 仍然挂载时才移除 try { _entry?.remove(); } catch (_) {} _entry = null; } void setForeground(bool fg) { _isForeground = fg; if (fg) _pump(); } } class GlobalModalHost extends StatefulWidget { final Widget child; const GlobalModalHost({super.key, required this.child}); @override State createState() => _GlobalModalHostState(); } class _GlobalModalHostState extends State with WidgetsBindingObserver { final GlobalKey _overlayKey = GlobalKey(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); GlobalModalController.I.setOverlayKey(_overlayKey); } @override void didChangeDependencies() { super.didChangeDependencies(); GlobalModalController.I.attachHost(context); } @override void didChangeAppLifecycleState(AppLifecycleState state) { GlobalModalController.I.setForeground(state == AppLifecycleState.resumed); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { return Overlay( key: _overlayKey, initialEntries: [ OverlayEntry(builder: (_) => widget.child), ], ); } } class _QueueItem { final int idx; final ModalRequest req; final Completer completer; _QueueItem({required this.idx, required this.req, required this.completer}); } // ========================== // 3) 通用弹窗壳 // ========================== class _ModalScope extends StatelessWidget { final Widget child; final VoidCallback onBackgroundTap; const _ModalScope({required this.child, required this.onBackgroundTap}); @override Widget build(BuildContext context) { return Stack( children: [ Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onBackgroundTap, child: AnimatedOpacity( opacity: 1, duration: const Duration(milliseconds: 150), child: Container(color: Colors.black54), ), ), ), Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 360), child: Card( elevation: 12, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(16), child: child, ), ), ), ), ], ); } } // ========================== // 4) 典型业务弹窗注册(好友申请 / 直播连麦) // ========================== void registerGlobalModals() { final gm = GlobalModalController.I; gm.register('friend_request', (context, req, complete) { final userName = req.payload['userName'] as String? ?? '某位用户'; final avatar = req.payload['avatar'] as String?; return Column( mainAxisSize: MainAxisSize.min, children: [ if (avatar != null) ...[ CircleAvatar(radius: 28, backgroundImage: NetworkImage(avatar)), const SizedBox(height: 12), ], Text('$userName 请求添加你为好友', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 8), const Text('是否接受?'), const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedButton( onPressed: () => complete(const ModalResult('decline')), child: const Text('拒绝'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () => complete(const ModalResult('accept')), child: const Text('同意'), ), ), ], ) ], ); }); gm.register('live_invite', (context, req, complete) { final roomName = req.payload['roomName'] ?? '直播间'; final inviter = req.payload['inviter'] ?? '主播'; final countdown = ValueNotifier(req.payload['countdownSec'] ?? 15); Timer? t; t = Timer.periodic(const Duration(seconds: 1), (_) { countdown.value = (countdown.value - 1).clamp(0, 999); if (countdown.value == 0) { t?.cancel(); complete(const ModalResult('timeout')); } }); return StatefulBuilder( builder: (context, setState) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text('$inviter 邀请你加入 $roomName 连麦', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 8), ValueListenableBuilder( valueListenable: countdown, builder: (_, v, __) => Text('将于 $v 秒后自动取消'), ), const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedButton( onPressed: () { t?.cancel(); complete(const ModalResult('decline')); }, child: const Text('稍后'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () { t?.cancel(); complete(const ModalResult('accept')); }, child: const Text('立即加入'), ), ), ], ) ], ); }, ); }); }