Skip to content

Commit 856cd7f

Browse files
Merge branch 'development' into issue1377
2 parents b21619a + 18a099e commit 856cd7f

27 files changed

+3713
-359
lines changed

.github/workflows/pull_request.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ on:
77
env:
88
ANDROID_EMULATOR_API: 34
99
ANDROID_EMULATOR_ARCH: x86_64
10-
IPHONE_DEVICE_MODEL: iPhone 16 Pro Max
11-
IPAD_DEVICE_MODEL: iPad Pro 13-inch (M4)
10+
IPHONE_DEVICE_MODEL: iPhone 16
11+
IPAD_DEVICE_MODEL: iPad (10th generation)
1212

1313
jobs:
1414
common:
@@ -48,7 +48,7 @@ jobs:
4848
- name: Set up Xcode
4949
uses: maxim-lobanov/setup-xcode@v1.6.0
5050
with:
51-
xcode-version: latest-stable
51+
xcode-version: 16.4.0
5252

5353
- uses: actions/checkout@v4
5454

@@ -74,10 +74,13 @@ jobs:
7474
- name: Set up Xcode
7575
uses: maxim-lobanov/setup-xcode@v1.6.0
7676
with:
77-
xcode-version: latest
77+
xcode-version: 16.4.0
7878

7979
- uses: actions/checkout@v4
8080

81+
- name: List available simulators
82+
run: xcrun simctl list devices
83+
8184
- name: iOS Screenshot Workflow
8285
uses: ./.github/actions/screenshot-ios
8386
with:

lib/bademagic_module/models/mode.dart

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ enum Mode {
44
up('0x02'),
55
down('0x03'),
66
fixed('0x04'),
7-
animation('0x05'),
8-
snowflake('0x06'),
9-
picture('0x07'),
10-
laser('0x08');
7+
snowflake('0x05'),
8+
picture('0x06'),
9+
animation('0x07'),
10+
laser('0x08'),
11+
pacman('0x09'), // Added Pacman mode
12+
chevronleft('0x0A'), // Chevron left mode
13+
diamond('0x0B'), // Diamond animation mode
14+
feet('0x0C'), // Feet animation mode
15+
brokenhearts('0x0D'), // Broken Hearts animation mode
16+
cupid('0x0E'); // Cupid animation mode
1117

1218
final String hexValue;
1319
const Mode(this.hexValue);

lib/bademagic_module/utils/byte_array_utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ List<int> hexStringToByteArray(String hexString) {
2525
int secondDigit = int.parse(hexString[i + 1], radix: 16);
2626
data.add((firstDigit << 4) + secondDigit);
2727
}
28-
logger.d(data.length);
28+
2929
return data;
3030
}
3131

lib/bademagic_module/utils/file_helper.dart

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,15 +302,16 @@ class FileHelper {
302302
file.path.endsWith('.json') &&
303303
!file.path.contains('data_')) {
304304
try {
305-
// Read the JSON file
306305
String jsonString = await file.readAsString();
307-
308-
// Convert JSON string to Data object
309306
Map<String, dynamic> jsonData = jsonDecode(jsonString);
310-
logger.d('JSON data: $jsonData');
311307

312-
// Add the Data object to the list with the filename as the key
313-
badgeDataList.add(MapEntry(file.path.split('/').last, jsonData));
308+
// Defensive: Only add if valid structure
309+
if (jsonData.containsKey('messages') &&
310+
jsonData['messages'] is List) {
311+
badgeDataList.add(MapEntry(file.path.split('/').last, jsonData));
312+
} else {
313+
logger.i('Skipping invalid badge file: ${file.path}');
314+
}
314315
} catch (e) {
315316
logger.i('Error parsing file ${file.path}: $e');
316317
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'package:badgemagic/badge_animation/animation_abstract.dart';
2+
3+
class BeatingHeartsAnimation extends BadgeAnimation {
4+
static const int badgeHeight = 11;
5+
static const int badgeWidth = 44;
6+
static const int hardwareFrameCount = 8;
7+
8+
// Define heart patterns for different sizes (bigger, more visible hearts)
9+
static const List<List<String>> heartPatterns = [
10+
// Size 1 (small heart - 5x4)
11+
["## ##", "#####", " ### ", " # "],
12+
// Size 2 (medium heart - 7x6)
13+
["### ###", "#######", "#######", " ##### ", " ### ", " # "],
14+
// Size 3 (large heart - 9x7) - refined shape
15+
[
16+
" ### ### ",
17+
"#########",
18+
"#########",
19+
" #######",
20+
" ##### ",
21+
" ### ",
22+
" # "
23+
],
24+
// Size 4 (extra large heart - 11x8) - refined shape
25+
[
26+
" #### #### ",
27+
"###########",
28+
"###########",
29+
" ######### ",
30+
" ####### ",
31+
" ##### ",
32+
" ### ",
33+
" # "
34+
]
35+
];
36+
37+
@override
38+
void processAnimation(
39+
int badgeHeight,
40+
int badgeWidth,
41+
int animationIndex,
42+
List<List<bool>> processGrid,
43+
List<List<bool>> canvas,
44+
) {
45+
// Clear the canvas
46+
for (int y = 0; y < badgeHeight; y++) {
47+
for (int x = 0; x < badgeWidth; x++) {
48+
canvas[y][x] = false;
49+
}
50+
}
51+
52+
// More dramatic heart scale values for better beating animation
53+
const List<double> heartScales = [0.1, 0.3, 0.5, 0.7, 1.0, 0.7, 0.5, 0.3];
54+
double scale = heartScales[animationIndex % hardwareFrameCount];
55+
56+
// Position hearts with better spacing for larger hearts
57+
int leftHeartCenterX = 11;
58+
int rightHeartCenterX = 33;
59+
int centerY = badgeHeight ~/ 2;
60+
61+
_drawHeart(canvas, leftHeartCenterX, centerY, scale);
62+
_drawHeart(canvas, rightHeartCenterX, centerY, scale);
63+
}
64+
65+
void _drawHeart(List<List<bool>> canvas, int cx, int cy, double scale) {
66+
// Determine which heart pattern to use based on scale
67+
int patternIndex;
68+
if (scale <= 0.2) {
69+
patternIndex = 0;
70+
} else if (scale <= 0.4) {
71+
patternIndex = 1;
72+
} else if (scale <= 0.7) {
73+
patternIndex = 2;
74+
} else {
75+
patternIndex = 3;
76+
}
77+
78+
List<String> pattern = heartPatterns[patternIndex];
79+
int patternHeight = pattern.length;
80+
int patternWidth = pattern[0].length;
81+
82+
int startY = cy - patternHeight ~/ 2;
83+
int startX = cx - patternWidth ~/ 2;
84+
85+
for (int py = 0; py < patternHeight; py++) {
86+
for (int px = 0; px < patternWidth; px++) {
87+
if (px < pattern[py].length && pattern[py][px] == '#') {
88+
int actualX = startX + px;
89+
int actualY = startY + py;
90+
91+
if (_inBounds(actualX, actualY)) {
92+
canvas[actualY][actualX] = true;
93+
}
94+
}
95+
}
96+
}
97+
98+
if (scale <= 0.1) {
99+
if (_inBounds(cx, cy)) canvas[cy][cx] = true;
100+
if (_inBounds(cx - 1, cy)) canvas[cy][cx - 1] = true;
101+
if (_inBounds(cx + 1, cy)) canvas[cy][cx + 1] = true;
102+
if (_inBounds(cx, cy - 1)) canvas[cy - 1][cx] = true;
103+
if (_inBounds(cx, cy + 1)) canvas[cy + 1][cx] = true;
104+
}
105+
}
106+
107+
bool _inBounds(int x, int y) {
108+
return x >= 0 && x < badgeWidth && y >= 0 && y < badgeHeight;
109+
}
110+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'package:badgemagic/badge_animation/animation_abstract.dart';
2+
import 'dart:math';
3+
4+
class BrokenHeartsAnimation extends BadgeAnimation {
5+
/// **Now a 9×9 heart** (instead of 7×7), so it fills more of the 11×44 badge.
6+
static const List<List<int>> heartShape = [
7+
[0, 0, 1, 1, 0, 1, 1, 0, 0],
8+
[0, 1, 1, 1, 1, 1, 1, 1, 0],
9+
[1, 1, 1, 1, 1, 1, 1, 1, 1],
10+
[1, 1, 1, 1, 1, 1, 1, 1, 1],
11+
[0, 1, 1, 1, 1, 1, 1, 1, 0],
12+
[0, 0, 1, 1, 1, 1, 1, 0, 0],
13+
[0, 0, 0, 1, 1, 1, 0, 0, 0],
14+
[0, 0, 0, 0, 1, 0, 0, 0, 0],
15+
[
16+
0,
17+
0,
18+
0,
19+
0,
20+
0,
21+
0,
22+
0,
23+
0,
24+
0
25+
], // tip row (optional—it ensures the very bottom pixel sits one row above the badge bottom)
26+
];
27+
28+
final List<List<Point<int>>> _clustersLeft = [];
29+
final List<List<Point<int>>> _clustersRight = [];
30+
bool _initialized = false;
31+
final Random _rng = Random(12345);
32+
33+
void _initializeClusters(int badgeH, int badgeW) {
34+
if (_initialized) return;
35+
_initialized = true;
36+
37+
final int heartW = heartShape[0].length;
38+
final int heartH = heartShape.length;
39+
final int leftCx = badgeW ~/ 4 - heartW ~/ 2;
40+
final int topY = badgeH ~/ 2 - heartH ~/ 2;
41+
42+
// collect all solid pixels of left heart
43+
final pixels = <Point<int>>[];
44+
for (int y = 0; y < heartH; y++) {
45+
for (int x = 0; x < heartW; x++) {
46+
if (heartShape[y][x] == 1) {
47+
pixels.add(Point(leftCx + x, topY + y));
48+
}
49+
}
50+
}
51+
52+
// carve into random clusters of size 1–4
53+
while (pixels.isNotEmpty) {
54+
int size = _rng.nextInt(min(4, pixels.length)) + 1;
55+
final clusterL = <Point<int>>[];
56+
for (int i = 0; i < size; i++) {
57+
clusterL.add(pixels.removeAt(_rng.nextInt(pixels.length)));
58+
}
59+
_clustersLeft.add(clusterL);
60+
_clustersRight
61+
.add(clusterL.map((pt) => Point(pt.x + badgeW ~/ 2, pt.y)).toList());
62+
}
63+
64+
// sort so bottom-most clusters fall first
65+
final paired = List.generate(
66+
_clustersLeft.length,
67+
(i) => MapEntry(_clustersLeft[i], _clustersRight[i]),
68+
);
69+
paired.sort((a, b) {
70+
double ya = a.key.map((p) => p.y).reduce((u, v) => u + v) / a.key.length;
71+
double yb = b.key.map((p) => p.y).reduce((u, v) => u + v) / b.key.length;
72+
return yb.compareTo(ya); // descending: larger Y first
73+
});
74+
_clustersLeft
75+
..clear()
76+
..addAll(paired.map((e) => e.key));
77+
_clustersRight
78+
..clear()
79+
..addAll(paired.map((e) => e.value));
80+
}
81+
82+
@override
83+
void processAnimation(
84+
int badgeHeight,
85+
int badgeWidth,
86+
int animationIndex,
87+
List<List<bool>> processGrid,
88+
List<List<bool>> canvas,
89+
) {
90+
_initializeClusters(badgeHeight, badgeWidth);
91+
92+
// clear
93+
for (int y = 0; y < badgeHeight; y++) {
94+
for (int x = 0; x < badgeWidth; x++) {
95+
canvas[y][x] = false;
96+
}
97+
}
98+
99+
final int N = _clustersLeft.length;
100+
final int cycle = N + badgeHeight;
101+
final int frame = animationIndex % cycle;
102+
103+
// draw each cluster either “attached” or “falling”
104+
for (int i = 0; i < N; i++) {
105+
final bool isFalling = frame >= i;
106+
final int dy = frame - i;
107+
for (var pt in _clustersLeft[i]) {
108+
final int y = isFalling ? pt.y + dy : pt.y;
109+
if (y >= 0 && y < badgeHeight) canvas[y][pt.x] = true;
110+
}
111+
for (var pt in _clustersRight[i]) {
112+
final int y = isFalling ? pt.y + dy : pt.y;
113+
if (y >= 0 && y < badgeHeight) canvas[y][pt.x] = true;
114+
}
115+
}
116+
}
117+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import 'package:badgemagic/badge_animation/animation_abstract.dart';
2+
3+
class LeftChevronAnimation extends BadgeAnimation {
4+
@override
5+
void processAnimation(int badgeHeight, int badgeWidth, int animationIndex,
6+
List<List<bool>> processGrid, List<List<bool>> canvas) {
7+
// Clear canvas
8+
for (int i = 0; i < badgeHeight; i++) {
9+
for (int j = 0; j < badgeWidth; j++) {
10+
canvas[i][j] = false;
11+
}
12+
}
13+
// Compact arrow: 4 columns wide, 7 rows tall
14+
int arrowWidth = 4;
15+
int arrowHeight = 7;
16+
int offset = animationIndex % arrowWidth;
17+
int arrowTop = (badgeHeight - arrowHeight) ~/ 2;
18+
// Arrow pattern for a compact '<' (4x7)
19+
List<List<bool>> arrow = [
20+
[false, false, false, true],
21+
[false, false, true, false],
22+
[false, true, false, false],
23+
[true, false, false, false],
24+
[false, true, false, false],
25+
[false, false, true, false],
26+
[false, false, false, true],
27+
];
28+
// Draw as many arrows as fit across the width, packed tightly
29+
for (int arrowIdx = 0;
30+
arrowIdx < (badgeWidth / arrowWidth).ceil() + 2;
31+
arrowIdx++) {
32+
int startCol = badgeWidth - offset - arrowIdx * arrowWidth;
33+
for (int y = 0; y < arrowHeight; y++) {
34+
for (int x = 0; x < arrowWidth; x++) {
35+
int row = arrowTop + y;
36+
int col = startCol + x;
37+
if (row >= 0 &&
38+
row < badgeHeight &&
39+
col >= 0 &&
40+
col < badgeWidth &&
41+
arrow[y][x]) {
42+
canvas[row][col] = true;
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)