Files
2025-09-10 00:07:52 +08:00

116 lines
3.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:ainoval/utils/web_theme.dart';
/// 公告轮播组件:
/// - 支持自动循环播放
/// - 鼠标悬停暂停
/// - 文本可选择复制
/// - 可手动添加消息
class NoticeTicker extends StatefulWidget {
final List<String>? initialMessages;
final Duration interval;
final TextStyle? textStyle;
final bool allowAdd;
const NoticeTicker({
super.key,
this.initialMessages,
this.interval = const Duration(seconds: 4),
this.textStyle,
this.allowAdd = false,
});
@override
State<NoticeTicker> createState() => _NoticeTickerState();
}
class _NoticeTickerState extends State<NoticeTicker> {
late List<String> _messages;
int _currentIndex = 0;
Timer? _timer;
bool _isHovering = false;
@override
void initState() {
super.initState();
_messages = (widget.initialMessages == null || widget.initialMessages!.isEmpty)
? <String>[
'当前小说网站属于测试状态欢迎大家加入qq群1062403092',
'如果有报错和bug或者改进建议欢迎大家在群里反馈'
]
: List<String>.from(widget.initialMessages!);
_startTimer();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startTimer() {
_timer?.cancel();
if (_messages.length <= 1) return;
_timer = Timer.periodic(widget.interval, (_) {
if (!_isHovering && mounted) {
setState(() {
_currentIndex = (_currentIndex + 1) % _messages.length;
});
}
});
}
@override
Widget build(BuildContext context) {
final style = widget.textStyle ?? TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: WebTheme.getPrimaryColor(context),
);
final current = _messages.isNotEmpty ? _messages[_currentIndex] : '';
return Container(
constraints: const BoxConstraints(minHeight: 40),
alignment: Alignment.centerLeft,
child: Row(
children: [
// 文本区域:悬停暂停 + 可复制
Expanded(
child: MouseRegion(
onEnter: (_) {
setState(() => _isHovering = true);
},
onExit: (_) {
setState(() => _isHovering = false);
},
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
transitionBuilder: (child, animation) {
// 轻微滑动+淡入
final offset = Tween<Offset>(begin: const Offset(0.1, 0), end: Offset.zero).animate(animation);
return ClipRect(
child: SlideTransition(position: offset, child: FadeTransition(opacity: animation, child: child)),
);
},
child: SelectableText(
current,
key: ValueKey<int>(_currentIndex),
style: style,
maxLines: 1,
textAlign: TextAlign.left,
toolbarOptions: const ToolbarOptions(copy: true, selectAll: true),
),
),
),
),
],
),
);
}
}