Files
desktop-plugin/tool/gen_app_icons.dart
T
dzc bee734fd5b feat: 后台托盘 + 资安信品牌化 + 修复布局断言(Phase 4 部分)
- 后台托盘(mac/Windows):window_manager 拦截关窗改隐藏,服务持续运行;
  托盘单击恢复窗口、右键菜单(显示主界面/退出),退出入口仅放托盘菜单
- 品牌:App 更名"资安信插件助手"(mac PRODUCT_NAME/xib、win main.cpp+Runner.rc(code_page 65001)、
  MaterialApp/侧栏/托盘 tooltip/日志);图标全家桶换资安信盾牌 logo
  (tool/gen_app_icons.dart 生成 mac AppIcon 7 尺寸/win ICO 7 尺寸/托盘白底圆盘版/assets logo)
- 修复启动布局断言:SliverList 内 Row(stretch) 需 IntrinsicHeight 包裹,
  否则子项收到 h=Infinity 约束(用户实测报错,test/ui_smoke_test.dart 守护复现)
- Agent.stop 幂等(托盘退出与窗口销毁竞态安全)
- 测试修复:smoke 测试改 tester.runAsync 包真实 socket I/O(fake async zone 里会永久挂起)、
  显式 pump 替代 pumpAndSettle;13 个测试全过,analyze 零问题,release 构建实测通过
2026-09-21 13:16:23 +08:00

144 lines
5.2 KiB
Dart
Raw 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.
// 生成全部应用图标(源:资安信 logo 1024x1024 深蓝盾牌+白色人形,透明底)
// dart run tool/gen_app_icons.dart <源图路径>
// 产物:
// macos/Runner/Assets.xcassets/AppIcon.appiconset/*.png (mac App 图标,白底圆角托+盾牌)
// windows/runner/resources/app_icon.ico (16~256 多尺寸 PNG-in-ICO)
// assets/tray_icon_mac.png / assets/tray_icon_win.png (托盘:白底圆盘+盾牌,深浅菜单栏均可见)
// assets/logo.png (原样拷贝,侧栏显示用)
// ignore_for_file: avoid_print
import 'dart:io';
import 'dart:typed_data';
import 'package:image/image.dart';
/// 是否在圆角矩形外(>0 外)
bool _cornerOutside(int x, int y, int x0, int y0, int x1, int y1, int r) {
final cx = x < x0 + r ? x0 + r : (x > x1 - r ? x1 - r : x);
final cy = y < y0 + r ? y0 + r : (y > y1 - r ? y1 - r : y);
final dx = x - cx, dy = y - cy;
if (dx > 0 || dy > 0) {
return dx * dx + dy * dy > r * r;
}
return false;
}
void main(List<String> args) {
final src = args.isNotEmpty ? args.first : 'logo-1024.png';
final logo = decodeImage(File(src).readAsBytesSync())!;
print('源图 ${logo.width}x${logo.height}');
// ---------- mac App 图标:白底圆角方托 + 盾牌居中 72% ----------
Image macIcon(int size) {
final img = Image(width: size, height: size, numChannels: 4);
final r = (size * 0.18).round(); // macOS squircle 风格圆角
final inset = (size * 0.14).round();
final inner = copyResize(logo, width: size - inset * 2);
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
if (!_cornerOutside(x, y, 0, 0, size - 1, size - 1, r)) {
final hasPixel = x >= inset &&
x < inset + inner.width &&
y >= inset &&
y < inset + inner.height;
final p = hasPixel ? inner.getPixel(x - inset, y - inset) : null;
if (p != null && p.a.toInt() > 0) {
img.setPixel(x, y, p);
} else {
img.setPixel(x, y, ColorRgba8(255, 255, 255, 255));
}
}
}
}
return img;
}
final appIconDir = Directory('macos/Runner/Assets.xcassets/AppIcon.appiconset');
const macSizes = <String, int>{
'app_icon_16.png': 16,
'app_icon_32.png': 32,
'app_icon_64.png': 64,
'app_icon_128.png': 128,
'app_icon_256.png': 256,
'app_icon_512.png': 512,
'app_icon_1024.png': 1024,
};
for (final e in macSizes.entries) {
final f = File('${appIconDir.path}/${e.key}');
if (!f.existsSync()) {
print('跳过(Contents.json 未声明):${e.key}');
continue;
}
f.writeAsBytesSync(encodePng(macIcon(e.value)));
print('written ${f.path} (${e.value}px)');
}
// ---------- Windows ICO(PNG-in-ICO) ----------
Uint8List buildIco(List<int> sizes) {
final pngs = <int, Uint8List>{};
for (final s in sizes) {
pngs[s] = Uint8List.fromList(encodePng(macIcon(s)));
}
final b = BytesBuilder();
b.addByte(0); b.addByte(0); // reserved
b.addByte(1); b.addByte(0); // type = icon
b.addByte(sizes.length); b.addByte(0); // count
var offset = 6 + 16 * sizes.length;
for (final s in sizes) {
final png = pngs[s]!;
b.addByte(s >= 256 ? 0 : s); // width
b.addByte(s >= 256 ? 0 : s); // height
b.addByte(0); // colors
b.addByte(0); // reserved
b.addByte(1); b.addByte(0); // planes
b.addByte(32); b.addByte(0); // bitcount
for (var i = 0; i < 4; i++) {
b.addByte((png.length >> (8 * i)) & 0xff);
}
for (var i = 0; i < 4; i++) {
b.addByte((offset >> (8 * i)) & 0xff);
}
offset += png.length;
}
for (final s in sizes) {
b.add(pngs[s]!);
}
return b.toBytes();
}
final ico = buildIco([16, 24, 32, 48, 64, 128, 256]);
File('windows/runner/resources/app_icon.ico').writeAsBytesSync(ico);
print('written windows/runner/resources/app_icon.ico (${ico.length} bytes)');
// ---------- 托盘:白底圆盘 + 盾牌居中 ----------
Image trayIcon(int size) {
final img = Image(width: size, height: size, numChannels: 4);
final c = (size - 1) / 2;
final rOuter = c;
final inset = (size * 0.13).round();
final inner = copyResize(logo, width: size - inset * 2);
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
final d = ((x - c) * (x - c) + (y - c) * (y - c));
if (d > rOuter * rOuter) continue; // 圆外透明
final hasPixel = x >= inset && x < inset + inner.width && y >= inset && y < inset + inner.height;
final p = hasPixel ? inner.getPixel(x - inset, y - inset) : null;
if (p != null && p.a.toInt() > 0) {
img.setPixel(x, y, p);
} else {
img.setPixel(x, y, ColorRgba8(255, 255, 255, 255));
}
}
}
return img;
}
Directory('assets').createSync(recursive: true);
File('assets/tray_icon_mac.png').writeAsBytesSync(encodePng(trayIcon(64)));
File('assets/tray_icon_win.png').writeAsBytesSync(encodePng(trayIcon(32)));
print('written assets/tray_icon_mac.png / tray_icon_win.png');
// ---------- 原样 logo ----------
File('assets/logo.png').writeAsBytesSync(encodePng(logo));
print('written assets/logo.png');
}