feat: Phase 2 — upgrade deps to null-safe, replace deprecated packages

- pubspec: bump all maintained deps to null-safe latest; flutter pub get resolves
- Remove 12 packages (0-ref or no null-safe): splashscreen, flappy_search_bar,
  searchable_dropdown, share, platform_detect, flutter_inappwebview, countdown,
  gender_selection, flutter_dash, smooth_star_rating, hovering, ffi
- Add flutter_rating_bar
- Keep discontinued-but-null-safe for now (Phase 5): pull_to_refresh, hive, catcher
- New null-safe custom components (API-compatible, callers change only imports):
  utils/countdown.dart, widgets/general/{hover_widget,gender_selection,
  smooth_star_rating,search_bar}.dart
- main.dart: drop dead SplashScreen _buildBody + _to field
- 23 consuming files: swap deprecated imports to local components

Null-safety migration + upgraded-API adaptation = Phase 3.
This commit is contained in:
2026-07-24 03:10:12 +08:00
parent 7ffcc848d0
commit 1a578c925d
37 changed files with 1252 additions and 574 deletions

View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
/// 自定义实现,替代已停维的 `gender_selection` 包。
///
/// 保留原 `Gender` 枚举与 `GenderSelection` 组件的命名参数,
/// 使调用方仅改 import 即可编译。大部分装饰性参数为兼容保留(暂未全部生效,
/// 后续美化阶段 Phase 5 再完善视觉)。
enum Gender { Male, Female }
class GenderSelection extends StatefulWidget {
final String? maleText;
final String? femaleText;
final Color? selectedGenderIconBackgroundColor;
final Alignment? checkIconAlignment;
final IconData? selectedGenderCheckIcon;
final void Function(Gender gender)? onChanged;
final bool? equallyAligned;
final Duration? animationDuration;
final bool? isCircular;
final bool? isSelectedGenderIconCircular;
final double? opacityOfGradient;
final EdgeInsets? padding;
final double? size;
final Gender? selectedGender;
const GenderSelection({
super.key,
this.maleText,
this.femaleText,
this.selectedGenderIconBackgroundColor,
this.checkIconAlignment,
this.selectedGenderCheckIcon,
this.onChanged,
this.equallyAligned,
this.animationDuration,
this.isCircular,
this.isSelectedGenderIconCircular,
this.opacityOfGradient,
this.padding,
this.size,
this.selectedGender,
});
@override
State<GenderSelection> createState() => _GenderSelectionState();
}
class _GenderSelectionState extends State<GenderSelection> {
Gender? _selected;
@override
void initState() {
super.initState();
_selected = widget.selectedGender;
}
Widget _buildOption(Gender gender, String label) {
final selected = _selected == gender;
final color = widget.selectedGenderIconBackgroundColor ?? Colors.indigo;
return Expanded(
child: Padding(
padding: widget.padding ?? const EdgeInsets.all(3.0),
child: InkWell(
onTap: () {
setState(() => _selected = gender);
widget.onChanged?.call(gender);
},
child: Container(
height: widget.size ?? 50.0,
alignment: Alignment.center,
decoration: BoxDecoration(
color: selected ? color : color.withOpacity(0.06),
borderRadius:
(widget.isCircular ?? false) ? BorderRadius.circular(60) : null,
border: Border.all(
color: selected ? color : Colors.black26,
width: selected ? 2 : 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (selected && widget.selectedGenderCheckIcon != null)
Icon(widget.selectedGenderCheckIcon, color: Colors.white, size: 18),
if (selected && widget.selectedGenderCheckIcon != null)
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: selected ? Colors.white : Colors.black87,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
_buildOption(Gender.Male, widget.maleText ?? 'Mr'),
_buildOption(Gender.Female, widget.femaleText ?? 'Ms'),
],
);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
/// 自定义实现,替代已停维的 `hovering` 包的 [HoverWidget]。
///
/// API 兼容:
/// ```dart
/// HoverWidget(
/// child: ...,
/// hoverChild: ...,
/// onHover: (PointerHoverEvent event) { ... },
/// )
/// ```
///
/// 鼠标悬停时显示 [hoverChild],否则显示 [child]。
class HoverWidget extends StatefulWidget {
final Widget child;
final Widget? hoverChild;
final void Function(PointerHoverEvent)? onHover;
const HoverWidget({
super.key,
required this.child,
this.hoverChild,
this.onHover,
});
@override
State<HoverWidget> createState() => _HoverWidgetState();
}
class _HoverWidgetState extends State<HoverWidget> {
bool _hovering = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
onHover: widget.onHover,
onEnter: (_) => setState(() => _hovering = true),
onExit: (_) => setState(() => _hovering = false),
child: (_hovering && widget.hoverChild != null)
? widget.hoverChild!
: widget.child,
);
}
}

View File

@@ -1,5 +1,5 @@
import 'package:countdown/countdown.dart';
import '../utils/countdown.dart';
import 'package:flutter/material.dart';
import 'package:pinput/pin_put/pin_put.dart';

View File

@@ -0,0 +1,185 @@
import 'dart:async';
import 'package:flutter/material.dart';
/// 轻量级异步搜索控制器,配合 [SearchBar] 使用。
///
/// 替代已停维的 `flappy_search_bar` 的 `SearchBarController`。
/// 保留泛型 `<T>` 以兼容调用方 `SearchBarController<Product>()` 写法。
/// [replayLastSearch] 用于“加载更多 / 分页”:外部 page++ 后触发重跑上一次搜索。
class SearchBarController<T> {
String lastQuery = '';
VoidCallback? _replay;
void _register(VoidCallback replay) {
_replay = replay;
}
/// 重新执行上一次搜索(分页加载更多时调用)。
void replayLastSearch() => _replay?.call();
}
/// 轻量级 Web 异步搜索栏,替代已停维的 `flappy_search_bar` 的 `SearchBar`。
///
/// 基本流程:输入达到 [minimumChars] 个字符后,调用 [onSearch] 取结果,
/// 用 [onItemFound] 渲染每一条;无结果时显示 [emptyWidget];出错显示 [onError]。
///
/// 注意:`searchBarController` 参数刻意使用裸类型(`SearchBarController?`
/// 以兼容调用方 `SearchBarController _controller = SearchBarController<Product>();`
/// 的写法Dart 3 sound 类型下避免泛型不变性报错)。
class SearchBar<T> extends StatefulWidget {
final SearchBarController? searchBarController;
final int minimumChars;
final String? hintText;
final Widget? cancellationWidget;
final Future<List<T>> Function(String text) onSearch;
final Widget Function(T item, int index) onItemFound;
final Widget Function(dynamic error)? onError;
final Widget? emptyWidget;
final Duration debounceDuration;
const SearchBar({
super.key,
this.searchBarController,
this.minimumChars = 2,
this.hintText,
this.cancellationWidget,
required this.onSearch,
required this.onItemFound,
this.onError,
this.emptyWidget,
this.debounceDuration = const Duration(milliseconds: 350),
});
@override
State<SearchBar<T>> createState() => _SearchBarState<T>();
}
class _SearchBarState<T> extends State<SearchBar<T>> {
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
Timer? _debounce;
List<T> _results = <T>[];
bool _loading = false;
dynamic _error;
bool _hasSearched = false;
@override
void initState() {
super.initState();
widget.searchBarController?._register(_runSearch);
_textController.addListener(_onTextChanged);
}
@override
void dispose() {
_debounce?.cancel();
_textController.dispose();
_focusNode.dispose();
super.dispose();
}
void _onTextChanged() {
final text = _textController.text;
_debounce?.cancel();
if (text.length >= widget.minimumChars) {
_debounce = Timer(widget.debounceDuration, _runSearch);
} else if (text.isEmpty) {
setState(() {
_results = <T>[];
_hasSearched = false;
_error = null;
});
}
}
Future<void> _runSearch() async {
final query = _textController.text;
if (query.length < widget.minimumChars) return;
if (widget.searchBarController != null) {
widget.searchBarController!.lastQuery = query;
}
setState(() {
_loading = true;
_error = null;
_hasSearched = true;
});
try {
final results = await widget.onSearch(query);
if (mounted) {
setState(() {
_results = results;
_loading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e;
_loading = false;
});
}
}
}
void _cancel() {
_textController.clear();
_focusNode.unfocus();
setState(() {
_results = <T>[];
_hasSearched = false;
_error = null;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
controller: _textController,
focusNode: _focusNode,
decoration: InputDecoration(
hintText: widget.hintText,
prefixIcon: const Icon(Icons.search),
suffixIcon: _textController.text.isNotEmpty
? GestureDetector(
onTap: _cancel,
child: widget.cancellationWidget ??
const Icon(Icons.close, size: 20),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
),
),
const SizedBox(height: 8),
Expanded(child: _buildBody()),
],
);
}
Widget _buildBody() {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return widget.onError != null
? widget.onError!(_error)
: Center(child: Text('$_error'));
}
if (!_hasSearched) {
return const SizedBox.shrink();
}
if (_results.isEmpty) {
return widget.emptyWidget ?? const SizedBox.shrink();
}
return ListView.builder(
itemCount: _results.length,
itemBuilder: (BuildContext context, int index) =>
widget.onItemFound(_results[index], index),
);
}
}

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
/// 自定义实现,替代已停维的 `smooth_star_rating` 包的 [SmoothStarRating]。
///
/// 保留原命名参数(`allowHalfRating`/`onRated`/`starCount`/`rating`/`size`/
/// `filledIconData`/`color`/`borderColor`/`spacing`),调用方仅改 import 即可。
class SmoothStarRating extends StatefulWidget {
final bool allowHalfRating;
final void Function(double)? onRated;
final int starCount;
final double rating;
final double size;
final IconData filledIconData;
final IconData? halfFilledIconData;
final IconData? defaultIconData;
final Color color;
final Color borderColor;
final double spacing;
const SmoothStarRating({
super.key,
this.allowHalfRating = false,
this.onRated,
this.starCount = 5,
this.rating = 0.0,
this.size = 24.0,
this.filledIconData = Icons.star,
this.halfFilledIconData,
this.defaultIconData,
this.color = Colors.orange,
this.borderColor = Colors.orange,
this.spacing = 0.0,
});
@override
State<SmoothStarRating> createState() => _SmoothStarRatingState();
}
class _SmoothStarRatingState extends State<SmoothStarRating> {
late double _rating;
@override
void initState() {
super.initState();
_rating = widget.rating;
}
@override
void didUpdateWidget(covariant SmoothStarRating oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.rating != widget.rating) {
_rating = widget.rating;
}
}
Widget _star(int index) {
IconData icon;
Color iconColor;
if (index < _rating.floor()) {
icon = widget.filledIconData;
iconColor = widget.color;
} else if (widget.allowHalfRating &&
index == _rating.floor() &&
(_rating - index) >= 0.5) {
icon = widget.halfFilledIconData ?? Icons.star_half;
iconColor = widget.color;
} else {
icon = widget.defaultIconData ?? Icons.star_border;
iconColor = widget.borderColor;
}
return GestureDetector(
onTap: () {
setState(() => _rating = (index + 1).toDouble());
widget.onRated?.call(_rating);
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: widget.spacing / 2),
child: Icon(icon, size: widget.size, color: iconColor),
),
);
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(widget.starCount, _star),
);
}
}

View File

@@ -3,7 +3,7 @@ import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_wisetronic/utils/utils.dart';
import 'package:hovering/hovering.dart';
import 'hover_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../routes.dart';