Local-first Flutter app that identifies the single behavioral change most likely to extend lifespan using hazard-based modeling. Features: - Risk engine with hazard ratios from meta-analyses - 50 countries mapped to 4 mortality groups - 6 modifiable factors: smoking, alcohol, sleep, activity, driving, work hours - SQLite local storage (no cloud, no accounts) - Muted clinical UI theme - 23 unit tests for risk engine Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
198 lines
5.5 KiB
Dart
198 lines
5.5 KiB
Dart
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
import 'dart:math';
|
|
|
|
/// Generates a simple tree icon PNG
|
|
void main() async {
|
|
const size = 1024;
|
|
final pixels = Uint8List(size * size * 4);
|
|
|
|
// Fill with white background
|
|
for (var i = 0; i < pixels.length; i += 4) {
|
|
pixels[i] = 255; // R
|
|
pixels[i + 1] = 255; // G
|
|
pixels[i + 2] = 255; // B
|
|
pixels[i + 3] = 255; // A
|
|
}
|
|
|
|
// Tree colors (muted teal from our theme)
|
|
const treeR = 74; // 0x4A
|
|
const treeG = 144; // 0x90
|
|
const treeB = 164; // 0xA4
|
|
|
|
// Trunk color (darker)
|
|
const trunkR = 90;
|
|
const trunkG = 70;
|
|
const trunkB = 55;
|
|
|
|
// Draw tree trunk (rectangle)
|
|
final trunkLeft = (size * 0.44).round();
|
|
final trunkRight = (size * 0.56).round();
|
|
final trunkTop = (size * 0.65).round();
|
|
final trunkBottom = (size * 0.85).round();
|
|
|
|
for (var y = trunkTop; y < trunkBottom; y++) {
|
|
for (var x = trunkLeft; x < trunkRight; x++) {
|
|
final i = (y * size + x) * 4;
|
|
pixels[i] = trunkR;
|
|
pixels[i + 1] = trunkG;
|
|
pixels[i + 2] = trunkB;
|
|
pixels[i + 3] = 255;
|
|
}
|
|
}
|
|
|
|
// Draw tree canopy (three triangles stacked)
|
|
void drawTriangle(int centerX, int topY, int height, int baseWidth) {
|
|
for (var y = topY; y < topY + height; y++) {
|
|
final progress = (y - topY) / height;
|
|
final halfWidth = (baseWidth * progress / 2).round();
|
|
for (var x = centerX - halfWidth; x <= centerX + halfWidth; x++) {
|
|
if (x >= 0 && x < size && y >= 0 && y < size) {
|
|
final i = (y * size + x) * 4;
|
|
pixels[i] = treeR;
|
|
pixels[i + 1] = treeG;
|
|
pixels[i + 2] = treeB;
|
|
pixels[i + 3] = 255;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final centerX = size ~/ 2;
|
|
|
|
// Top triangle (smallest)
|
|
drawTriangle(centerX, (size * 0.15).round(), (size * 0.20).round(), (size * 0.35).round());
|
|
|
|
// Middle triangle
|
|
drawTriangle(centerX, (size * 0.28).round(), (size * 0.22).round(), (size * 0.48).round());
|
|
|
|
// Bottom triangle (largest)
|
|
drawTriangle(centerX, (size * 0.42).round(), (size * 0.26).round(), (size * 0.58).round());
|
|
|
|
// Encode as PNG
|
|
final png = encodePng(size, size, pixels);
|
|
|
|
// Write to file
|
|
final file = File('assets/icon/app_icon.png');
|
|
await file.writeAsBytes(png);
|
|
print('Generated app_icon.png');
|
|
|
|
// Also create foreground version (same but smaller for adaptive icons)
|
|
final foregroundFile = File('assets/icon/app_icon_foreground.png');
|
|
await foregroundFile.writeAsBytes(png);
|
|
print('Generated app_icon_foreground.png');
|
|
}
|
|
|
|
/// Simple PNG encoder (no compression for simplicity)
|
|
Uint8List encodePng(int width, int height, Uint8List rgba) {
|
|
final output = BytesBuilder();
|
|
|
|
// PNG signature
|
|
output.add([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
|
|
|
// IHDR chunk
|
|
final ihdr = BytesBuilder();
|
|
ihdr.add(_int32be(width));
|
|
ihdr.add(_int32be(height));
|
|
ihdr.addByte(8); // bit depth
|
|
ihdr.addByte(6); // color type (RGBA)
|
|
ihdr.addByte(0); // compression
|
|
ihdr.addByte(0); // filter
|
|
ihdr.addByte(0); // interlace
|
|
_writeChunk(output, 'IHDR', ihdr.toBytes());
|
|
|
|
// IDAT chunk (image data with zlib compression)
|
|
// For simplicity, we'll use store (no compression)
|
|
final rawData = BytesBuilder();
|
|
for (var y = 0; y < height; y++) {
|
|
rawData.addByte(0); // filter type: None
|
|
for (var x = 0; x < width; x++) {
|
|
final i = (y * width + x) * 4;
|
|
rawData.addByte(rgba[i]); // R
|
|
rawData.addByte(rgba[i + 1]); // G
|
|
rawData.addByte(rgba[i + 2]); // B
|
|
rawData.addByte(rgba[i + 3]); // A
|
|
}
|
|
}
|
|
|
|
final compressed = _deflateStore(rawData.toBytes());
|
|
_writeChunk(output, 'IDAT', compressed);
|
|
|
|
// IEND chunk
|
|
_writeChunk(output, 'IEND', Uint8List(0));
|
|
|
|
return output.toBytes();
|
|
}
|
|
|
|
Uint8List _int32be(int value) {
|
|
return Uint8List.fromList([
|
|
(value >> 24) & 0xFF,
|
|
(value >> 16) & 0xFF,
|
|
(value >> 8) & 0xFF,
|
|
value & 0xFF,
|
|
]);
|
|
}
|
|
|
|
void _writeChunk(BytesBuilder output, String type, Uint8List data) {
|
|
output.add(_int32be(data.length));
|
|
final typeBytes = type.codeUnits;
|
|
output.add(typeBytes);
|
|
output.add(data);
|
|
|
|
// CRC32 of type + data
|
|
final crcData = Uint8List(typeBytes.length + data.length);
|
|
crcData.setAll(0, typeBytes);
|
|
crcData.setAll(typeBytes.length, data);
|
|
output.add(_int32be(_crc32(crcData)));
|
|
}
|
|
|
|
/// Simple deflate with store (no compression)
|
|
Uint8List _deflateStore(Uint8List data) {
|
|
final output = BytesBuilder();
|
|
|
|
// zlib header
|
|
output.addByte(0x78); // CMF
|
|
output.addByte(0x01); // FLG (no dict, fastest)
|
|
|
|
// Split into blocks of max 65535 bytes
|
|
const maxBlock = 65535;
|
|
var offset = 0;
|
|
|
|
while (offset < data.length) {
|
|
final remaining = data.length - offset;
|
|
final blockSize = remaining > maxBlock ? maxBlock : remaining;
|
|
final isLast = offset + blockSize >= data.length;
|
|
|
|
output.addByte(isLast ? 0x01 : 0x00); // BFINAL + BTYPE (store)
|
|
output.addByte(blockSize & 0xFF);
|
|
output.addByte((blockSize >> 8) & 0xFF);
|
|
output.addByte((~blockSize) & 0xFF);
|
|
output.addByte(((~blockSize) >> 8) & 0xFF);
|
|
|
|
output.add(data.sublist(offset, offset + blockSize));
|
|
offset += blockSize;
|
|
}
|
|
|
|
// Adler-32 checksum
|
|
var s1 = 1;
|
|
var s2 = 0;
|
|
for (var i = 0; i < data.length; i++) {
|
|
s1 = (s1 + data[i]) % 65521;
|
|
s2 = (s2 + s1) % 65521;
|
|
}
|
|
final adler = (s2 << 16) | s1;
|
|
output.add(_int32be(adler));
|
|
|
|
return output.toBytes();
|
|
}
|
|
|
|
int _crc32(Uint8List data) {
|
|
var crc = 0xFFFFFFFF;
|
|
for (var byte in data) {
|
|
crc ^= byte;
|
|
for (var i = 0; i < 8; i++) {
|
|
crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320 : crc >> 1;
|
|
}
|
|
}
|
|
return crc ^ 0xFFFFFFFF;
|
|
}
|