country_icon.dart 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_svg/flutter_svg.dart';
  4. import 'package:nomo/app/extensions/widget_extension.dart';
  5. import 'ix_image.dart';
  6. /// 国旗/图标组件
  7. ///
  8. /// 智能查找逻辑:
  9. /// 1. 优先从 assets/flags/{countryCode}.svg 查找
  10. /// 2. 找不到则从 assets/images/streaming/{countryCode}.png 查找
  11. /// 3. 都找不到则显示默认图标 assets/flags/xx.svg
  12. class CountryIcon extends StatefulWidget {
  13. /// 国家/地区代码(如: us, cn, netflix 等)
  14. final String countryCode;
  15. /// 宽度
  16. final double width;
  17. /// 高度
  18. final double height;
  19. /// 圆角
  20. final double? borderRadius;
  21. /// 适配方式
  22. final BoxFit? fit;
  23. const CountryIcon({
  24. super.key,
  25. required this.countryCode,
  26. required this.width,
  27. required this.height,
  28. this.borderRadius,
  29. this.fit,
  30. });
  31. @override
  32. State<CountryIcon> createState() => _CountryIconState();
  33. }
  34. class _CountryIconState extends State<CountryIcon> {
  35. /// 静态缓存:存储已查找过的图片路径,避免重复异步检查
  36. static final Map<String, _ImageInfo> _imageCache = {};
  37. /// 图片路径
  38. String? _imagePath;
  39. /// 图片类型:svg 或 png
  40. String? _imageType;
  41. /// 是否首次加载(仅首次加载时显示占位符)
  42. bool _isFirstLoad = true;
  43. @override
  44. void initState() {
  45. super.initState();
  46. _findImage();
  47. }
  48. @override
  49. void didUpdateWidget(CountryIcon oldWidget) {
  50. super.didUpdateWidget(oldWidget);
  51. if (oldWidget.countryCode != widget.countryCode) {
  52. _findImage();
  53. }
  54. }
  55. /// 智能查找图片
  56. Future<void> _findImage() async {
  57. if (!mounted) return;
  58. final code = widget.countryCode.toLowerCase().trim();
  59. // 如果为空,直接使用默认图标
  60. if (code.isEmpty) {
  61. _updateImage('assets/flags/xx.svg', 'svg');
  62. return;
  63. }
  64. // 检查缓存,如果有缓存则直接使用,避免异步等待
  65. if (_imageCache.containsKey(code)) {
  66. final cached = _imageCache[code]!;
  67. _updateImage(cached.path, cached.type);
  68. return;
  69. }
  70. // 异步查找图片(不设置 loading 状态,保持旧图片显示)
  71. String? foundPath;
  72. String? foundType;
  73. // 1. 先检查 flags 目录的 svg
  74. final flagPath = 'assets/flags/$code.svg';
  75. if (await _assetExists(flagPath)) {
  76. foundPath = flagPath;
  77. foundType = 'svg';
  78. } else {
  79. // 2. 检查 streaming 目录的 png
  80. final streamingPath = 'assets/images/streaming/$code.png';
  81. if (await _assetExists(streamingPath)) {
  82. foundPath = streamingPath;
  83. foundType = 'png';
  84. } else {
  85. // 3. 使用默认图标
  86. foundPath = 'assets/flags/xx.svg';
  87. foundType = 'svg';
  88. }
  89. }
  90. // 缓存结果
  91. _imageCache[code] = _ImageInfo(foundPath, foundType);
  92. // 检查 widget 是否仍然需要这个 code 的图片(防止快速切换时的竞态)
  93. if (!mounted || widget.countryCode.toLowerCase().trim() != code) return;
  94. _updateImage(foundPath, foundType);
  95. }
  96. /// 更新图片状态
  97. void _updateImage(String path, String type) {
  98. if (!mounted) return;
  99. setState(() {
  100. _imagePath = path;
  101. _imageType = type;
  102. _isFirstLoad = false;
  103. });
  104. }
  105. /// 检查资源文件是否存在
  106. Future<bool> _assetExists(String path) async {
  107. try {
  108. await rootBundle.load(path);
  109. return true;
  110. } catch (e) {
  111. return false;
  112. }
  113. }
  114. @override
  115. Widget build(BuildContext context) {
  116. // 仅首次加载时显示占位符,切换时保持旧图片
  117. if (_isFirstLoad || _imagePath == null || _imageType == null) {
  118. return _buildPlaceholder().withShadow(
  119. borderRadius: widget.borderRadius ?? 0,
  120. );
  121. }
  122. // 根据图片类型返回不同的组件
  123. if (_imageType == 'svg') {
  124. return ClipRRect(
  125. borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
  126. child: SvgPicture.asset(
  127. _imagePath!,
  128. width: widget.width,
  129. height: widget.height,
  130. fit: widget.fit ?? BoxFit.cover,
  131. placeholderBuilder: (context) => _buildPlaceholder(),
  132. ),
  133. ).withShadow(borderRadius: widget.borderRadius ?? 0);
  134. } else {
  135. // png 类型使用 IXImage
  136. return IXImage(
  137. source: _imagePath!,
  138. sourceType: ImageSourceType.asset,
  139. width: widget.width,
  140. height: widget.height,
  141. borderRadius: widget.borderRadius,
  142. fit: widget.fit,
  143. ).withShadow(borderRadius: widget.borderRadius ?? 0);
  144. }
  145. }
  146. /// 构建占位符
  147. Widget _buildPlaceholder() {
  148. return Container(
  149. width: widget.width,
  150. height: widget.height,
  151. decoration: BoxDecoration(
  152. color: Colors.grey.withValues(alpha: 0.2),
  153. borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
  154. ),
  155. );
  156. }
  157. }
  158. /// 国旗/图标组件的简化版本(同步加载,不检查文件是否存在)
  159. ///
  160. /// 性能更好,但可能在资源不存在时显示错误
  161. class CountryIconFast extends StatelessWidget {
  162. /// 国家/地区代码
  163. final String countryCode;
  164. /// 宽度
  165. final double width;
  166. /// 高度
  167. final double height;
  168. /// 圆角
  169. final double? borderRadius;
  170. /// 适配方式
  171. final BoxFit? fit;
  172. /// 是否优先使用 streaming 目录
  173. final bool preferStreaming;
  174. const CountryIconFast({
  175. super.key,
  176. required this.countryCode,
  177. required this.width,
  178. required this.height,
  179. this.borderRadius,
  180. this.fit,
  181. this.preferStreaming = false,
  182. });
  183. @override
  184. Widget build(BuildContext context) {
  185. final code = countryCode.toLowerCase().trim();
  186. if (code.isEmpty) {
  187. return _buildSvg('assets/flags/xx.svg');
  188. }
  189. // 如果优先使用 streaming
  190. if (preferStreaming) {
  191. return _buildPng('assets/images/streaming/$code.png');
  192. }
  193. // 默认优先使用 flags
  194. return _buildSvg('assets/flags/$code.svg');
  195. }
  196. Widget _buildSvg(String path) {
  197. return ClipRRect(
  198. borderRadius: BorderRadius.circular(borderRadius ?? 0),
  199. child: SvgPicture.asset(
  200. path,
  201. width: width,
  202. height: height,
  203. fit: fit ?? BoxFit.cover,
  204. placeholderBuilder: (context) => _buildFallback(),
  205. ),
  206. );
  207. }
  208. Widget _buildPng(String path) {
  209. return IXImage(
  210. source: path,
  211. sourceType: ImageSourceType.asset,
  212. width: width,
  213. height: height,
  214. borderRadius: borderRadius,
  215. fit: fit,
  216. );
  217. }
  218. Widget _buildFallback() {
  219. // 降级到默认图标
  220. return SvgPicture.asset(
  221. 'assets/flags/xx.svg',
  222. width: width,
  223. height: height,
  224. fit: fit ?? BoxFit.cover,
  225. );
  226. }
  227. }
  228. /// 图片信息缓存类
  229. class _ImageInfo {
  230. final String path;
  231. final String type;
  232. _ImageInfo(this.path, this.type);
  233. }