import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; /// 双色图标组件。 /// /// 图标是 SVG 资源(`assets/icons/*.svg`),由 `tools/make_icons.py` 生成 —— /// 改形状或配色请改那个脚本再重跑,不要手工编辑 SVG 文件。 /// /// 每个图标自带一套配色(浅色填充 + 深色描边)。需要单色时传 [tint], /// 它会把整个图标压成一种颜色 —— 比如放在焦糖色主按钮上的白色图标。 class CuteIcon extends StatelessWidget { const CuteIcon(this.name, {super.key, this.size = 22, this.tint}); /// 图标名,见 [cuteIconNames] final String name; final double size; /// 传了就压成单色(丢失双色效果,但能适配任意背景) final Color? tint; @override Widget build(BuildContext context) { final resolved = resolveIconName(name); return SvgPicture.asset( 'assets/icons/$resolved.svg', width: size, height: size, colorFilter: tint == null ? null : ColorFilter.mode(tint!, BlendMode.srcIn), ); } } /// 所有合法的图标名(和 tools/make_icons.py 里的 ICONS 一一对应) const cuteIconNames = { // 记账分类 'food', 'transport', 'shopping', 'entertainment', 'medical', 'education', 'housing', 'utilities', 'phone', 'other', 'salary', 'bonus', 'invest', 'parttime', // 模块 'bill', 'note', 'assistant', // 界面 'expense', 'income', 'camera', 'search', 'user', 'lock', 'eye', 'image', 'file', 'folder', 'folder-open', 'check', 'pin', 'target', 'inbox', 'error', }; /// 老数据的兼容映射:emoji → 图标名。 /// /// 分类图标以前是 emoji 存在数据库里(`category.icon`),现在换成了图标名。 /// 这份映射有两个作用: /// ① 迁移还没跑的时候,老数据照样能显示 /// ② 迁移跑完之后,用户自己建分类时随手挑个 emoji 也能兜住 /// 都不认识就退到 `other`,不会出现空白图标。 /// /// 公开出来是为了能写测试(映射表里写错一个名字会静默退化成兜底图标, /// 不测的话很难发现)。 const cuteEmojiAliases = { '🍔': 'food', '🍜': 'food', '🍚': 'food', '🍕': 'food', '☕': 'food', '🚗': 'transport', '🚌': 'transport', '🚇': 'transport', '✈': 'transport', '🛍': 'shopping', '🛒': 'shopping', '👕': 'shopping', '🎁': 'bonus', '🎬': 'entertainment', '🎮': 'entertainment', '🎵': 'entertainment', '🏥': 'medical', '💊': 'medical', '💉': 'medical', '📚': 'education', '🎓': 'education', '📖': 'education', '✏': 'education', '🏠': 'housing', '🏡': 'housing', '🛏': 'housing', '💡': 'utilities', '💧': 'utilities', '⚡': 'utilities', '🔌': 'utilities', '📱': 'phone', '☎': 'phone', '💻': 'phone', '💰': 'salary', '💵': 'salary', '🏦': 'salary', '💴': 'salary', '📈': 'invest', '📊': 'invest', '💹': 'invest', '🪙': 'invest', '💼': 'parttime', '🧑‍💻': 'parttime', '🔧': 'parttime', '📦': 'other', '📌': 'other', '🏷': 'other', '❓': 'other', }; /// 把任意来源的图标值(图标名 / emoji / 空)解析成一个合法的图标名 String resolveIconName(String? raw) { final value = (raw ?? '').trim(); if (value.isEmpty) return 'other'; if (cuteIconNames.contains(value)) return value; // emoji 里可能带变体选择符(U+FE0F),去掉再查一次 final stripped = value.replaceAll('\uFE0F', ''); return cuteEmojiAliases[value] ?? cuteEmojiAliases[stripped] ?? 'other'; }