216 lines
6.5 KiB
Dart
216 lines
6.5 KiB
Dart
import 'dart:collection';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'CacheAavatar.dart';
|
|
import 'StringTool.dart';
|
|
|
|
// 全局消息显示时长(可修改)
|
|
Duration GlobalMessage_BakDuration = const Duration(seconds: 5);
|
|
|
|
class GlobalData {
|
|
final String? image;
|
|
final String title;
|
|
final String content;
|
|
final dynamic payload;
|
|
|
|
GlobalData({
|
|
this.image,
|
|
required this.title,
|
|
required this.content,
|
|
this.payload,
|
|
});
|
|
}
|
|
|
|
class GlobalMessage_Bak {
|
|
static final GlobalMessage_Bak _instance = GlobalMessage_Bak._internal();
|
|
static BuildContext? _context;
|
|
|
|
// 消息队列
|
|
static final Queue<_MessageItem> _queue = Queue();
|
|
|
|
factory GlobalMessage_Bak() => _instance;
|
|
GlobalMessage_Bak._internal();
|
|
|
|
static void init(BuildContext context) {
|
|
_context = context;
|
|
}
|
|
|
|
static void push({
|
|
required GlobalData data,
|
|
required Function(BuildContext) onTap,
|
|
BuildContext? context,
|
|
Duration? duration,
|
|
}) {
|
|
final useContext = context ?? _context;
|
|
if (useContext == null) {
|
|
throw Exception('GlobalMessage_Bak not initialized. Call GlobalMessage_Bak.init() first.');
|
|
}
|
|
|
|
final controller = AnimationController(
|
|
vsync: Navigator.of(useContext),
|
|
duration: const Duration(milliseconds: 300),
|
|
);
|
|
|
|
final slideAnimation = Tween<Offset>(
|
|
begin: const Offset(0, -1),
|
|
end: Offset.zero,
|
|
).animate(CurvedAnimation(parent: controller, curve: Curves.easeOut));
|
|
|
|
final entry = OverlayEntry(
|
|
builder: (_) => Positioned(
|
|
top: MediaQuery.of(useContext).padding.top,
|
|
left: 0,
|
|
right: 0,
|
|
child: SlideTransition(
|
|
position: slideAnimation,
|
|
child: _MessageContent(
|
|
data: data,
|
|
onTap: () {
|
|
GlobalMessage_Bak.hide(); // 统一通过 GlobalMessage_Bak.hide() 隐藏
|
|
onTap(useContext);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// 添加到队列
|
|
_queue.add(_MessageItem(entry: entry, controller: controller, duration: duration ?? GlobalMessage_BakDuration));
|
|
|
|
// 如果队列只有这一条,立即显示
|
|
if (_queue.length == 1) {
|
|
_showNext();
|
|
}
|
|
}
|
|
|
|
static void _showNext() {
|
|
if (_queue.isEmpty) return;
|
|
final item = _queue.first;
|
|
Overlay.of(_context!)!.insert(item.entry);
|
|
item.controller.forward();
|
|
|
|
// 自动隐藏
|
|
Future.delayed(item.duration, () => hideCurrent());
|
|
}
|
|
|
|
static void hideCurrent() {
|
|
if (_queue.isEmpty) return;
|
|
final item = _queue.first;
|
|
item.controller.reverse().then((_) {
|
|
item.entry.remove();
|
|
item.controller.dispose();
|
|
_queue.removeFirst();
|
|
_showNext(); // 显示队列中的下一条消息
|
|
});
|
|
}
|
|
|
|
// 手动隐藏当前显示的消息
|
|
static void hide() => hideCurrent();
|
|
}
|
|
|
|
// 队列中的消息项
|
|
class _MessageItem {
|
|
final OverlayEntry entry;
|
|
final AnimationController controller;
|
|
final Duration duration;
|
|
|
|
_MessageItem({required this.entry, required this.controller, required this.duration});
|
|
}
|
|
|
|
class _MessageContent extends StatefulWidget {
|
|
final GlobalData data;
|
|
final VoidCallback onTap;
|
|
|
|
const _MessageContent({Key? key, required this.data, required this.onTap}) : super(key: key);
|
|
|
|
@override
|
|
__MessageContentState createState() => __MessageContentState();
|
|
}
|
|
|
|
class __MessageContentState extends State<_MessageContent> {
|
|
late Offset _dragStartOffset;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final now = DateTime.now();
|
|
final formattedTime = DateFormat('HH:mm').format(now);
|
|
|
|
return GestureDetector(
|
|
onTap: widget.onTap,
|
|
onVerticalDragStart: (details) {
|
|
_dragStartOffset = details.globalPosition;
|
|
},
|
|
onVerticalDragUpdate: (details) {
|
|
if (details.globalPosition.dy < _dragStartOffset.dy - 50) {
|
|
GlobalMessage_Bak.hide();
|
|
}
|
|
},
|
|
child: Dismissible(
|
|
key: Key(DateTime.now().millisecondsSinceEpoch.toString()),
|
|
direction: DismissDirection.horizontal,
|
|
onDismissed: (direction) => GlobalMessage_Bak.hide(),
|
|
background: Container(color: Colors.transparent),
|
|
child: Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12.0),
|
|
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 4))],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (widget.data.image != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 16.0),
|
|
child: CacheAvatar(url: widget.data.image!,),
|
|
),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.data.title,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
color: Colors.black87,
|
|
decoration: TextDecoration.none),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Text(
|
|
formattedTime,
|
|
style: const TextStyle(fontSize: 12, color: Colors.grey,decoration: TextDecoration.none),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
StringTool.stripMarkdown(widget.data.content),
|
|
style: TextStyle(
|
|
color: Colors.grey[500],
|
|
fontSize: 12,
|
|
decoration: TextDecoration.none),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|