You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
119 lines
5.3 KiB
119 lines
5.3 KiB
import 'package:flutter/services.dart';
|
|
|
|
import '../../core/utils/logger.dart';
|
|
|
|
/// 按设备名解析产品图与产品小图标。
|
|
///
|
|
/// 约定:`assets/images/devices/<设备名>.png` 是主图,
|
|
/// `assets/images/devices/<设备名>_icon.png` 是同一产品的小图标。
|
|
/// 加新产品只要把两张图按这个命名丢进目录即可 —— pubspec 里
|
|
/// `assets/images/devices/` 是**按目录**声明的,新文件会自动打包,不用改配置。
|
|
///
|
|
/// ⚠️ 图片是打进安装包的,所以**新产品必须发版才能看到**。
|
|
/// 服务端 `DBProduct.productimage` 那条网络图的路子(后台传图、老包也能看到)
|
|
/// 是刻意没走的,见 2026-09-08 的决定。
|
|
class DeviceImages {
|
|
DeviceImages._();
|
|
|
|
static const String _tag = 'DeviceImages';
|
|
/// ⚠️ 目录里的图**必须是去背的透明 PNG**,`default.png` / `default_icon.png`
|
|
/// 也不例外。设备页的展示区是横宽的(约 1.14:1),而出图习惯是竖版海报
|
|
/// (892×1152,0.77:1)——带不透明底色的竖图用 `BoxFit.contain` 塞进去,
|
|
/// 会变成中间一条窄的色块卡片、左右大片留白,且底色与页面背景(尤其深色模式)
|
|
/// 硬碰硬。2026-09-14 之前的默认图正是这样,表现为「图片和显示框对不上」。
|
|
/// 去背之后主体自己铺满框、周围就是页面背景,不需要任何代码适配。
|
|
|
|
static const String dir = 'assets/images/devices/';
|
|
static const String defaultImage = '${dir}default.png';
|
|
static const String defaultIcon = '${dir}default_icon.png';
|
|
|
|
static const String _iconSuffix = '_icon';
|
|
|
|
/// 目录下所有产品主图的名字(不含 `.png`,不含 `_icon` 那些)
|
|
static final Set<String> _mainNames = <String>{};
|
|
|
|
/// 目录下实际存在的全部条目(含 `_icon`),用来判断小图标在不在
|
|
static final Set<String> _allNames = <String>{};
|
|
|
|
static bool _loaded = false;
|
|
|
|
/// 读一次 AssetManifest,把目录里实际有哪些图记下来。
|
|
///
|
|
/// 为什么要这一步:Flutter 没法同步判断某个 asset 存不存在。不预先知道
|
|
/// 有哪些图,就只能靠 `Image.asset` 的 errorBuilder 在渲染失败时兜底——
|
|
/// 那是**静默**的,美术漏出一张图或者命名对不上,界面直接变成通用图,
|
|
/// 没有任何地方能看出来。有了清单就能在解析时打一条 warn。
|
|
static Future<void> load() async {
|
|
try {
|
|
final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);
|
|
for (final key in manifest.listAssets()) {
|
|
if (!key.startsWith(dir) || !key.endsWith('.png')) continue;
|
|
final base = key.substring(dir.length, key.length - 4);
|
|
if (base.isEmpty) continue;
|
|
_allNames.add(base);
|
|
if (!base.endsWith(_iconSuffix)) _mainNames.add(base);
|
|
}
|
|
_loaded = true;
|
|
Logger.i(_tag, '产品图清单已载入:${_mainNames.length} 个产品 ${_allNames.length} 个文件');
|
|
} catch (e) {
|
|
Logger.w(_tag, '读取产品图清单失败,全部退回默认图: $e');
|
|
}
|
|
}
|
|
|
|
/// 设备主图。匹配不上返回 [defaultImage]。
|
|
static String imageFor(String? deviceName) {
|
|
final match = _match(deviceName);
|
|
return match == null ? defaultImage : '$dir$match.png';
|
|
}
|
|
|
|
/// 设备小图标。
|
|
///
|
|
/// 顺序:`<名字>_icon.png` → `<名字>.png` → `default_icon.png`。
|
|
/// 中间那一档是刻意留的:产品有主图但美术没出小图时,用主图缩着显示
|
|
/// 也比掉成通用图强 —— 掉通用图会让用户以为「App 不认识我的设备」。
|
|
/// 走到这一档会打 warn,提醒把 `_icon` 补上。
|
|
static String iconFor(String? deviceName) {
|
|
final match = _match(deviceName);
|
|
if (match == null) return defaultIcon;
|
|
final icon = '$match$_iconSuffix';
|
|
if (_allNames.contains(icon)) return '$dir$icon.png';
|
|
Logger.w(_tag, '产品 $match 缺小图标 $icon.png,暂用主图顶替');
|
|
return '$dir$match.png';
|
|
}
|
|
|
|
/// 把设备名匹配到目录里的某个产品名;匹配不上返回 null。
|
|
///
|
|
/// 三级,从严到宽:
|
|
/// 1. 完全相同(`Echo-one` → `Echo-one.png`)
|
|
/// 2. 忽略大小写(防美术出图时大小写和固件广播名不一致)
|
|
/// 3. 最长前缀(`Echo-one_L`、`Echo-one-3F2A` 这类带后缀的广播名)
|
|
///
|
|
/// ⚠️ 第 3 级必须取**最长**匹配。按任意顺序取第一个命中的话,
|
|
/// 目录里若同时有 `Echo` 和 `Echo-one`,`Echo-one` 的设备可能被 `Echo` 抢走。
|
|
static String? _match(String? deviceName) {
|
|
final name = (deviceName ?? '').trim();
|
|
if (name.isEmpty) return null;
|
|
if (!_loaded) {
|
|
// 清单还没载入(理论上不会发生,load() 在 runApp 之前 await)
|
|
Logger.w(_tag, '产品图清单未载入就来取图,先退默认:$name');
|
|
return null;
|
|
}
|
|
|
|
if (_mainNames.contains(name)) return name;
|
|
|
|
final lower = name.toLowerCase();
|
|
for (final n in _mainNames) {
|
|
if (n.toLowerCase() == lower) return n;
|
|
}
|
|
|
|
String? best;
|
|
for (final n in _mainNames) {
|
|
if (n.isEmpty || !lower.startsWith(n.toLowerCase())) continue;
|
|
if (best == null || n.length > best.length) best = n;
|
|
}
|
|
if (best == null) {
|
|
Logger.w(_tag, '没有 $name 对应的产品图,退回默认图');
|
|
}
|
|
return best;
|
|
}
|
|
}
|
|
|