- 后台托盘(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 构建实测通过
144 lines
5.2 KiB
Dart
144 lines
5.2 KiB
Dart
// 生成全部应用图标(源:资安信 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');
|
||
}
|