Skip to content

Commit 3533ec0

Browse files
committed
feat: add clipart export/import and badge integration
- Add shareClipartData and importClipartData to FileHelper - Add Import button and share icon in Saved Cliparts screen - Add Saved Cliparts tab in Create Badge screen with apply/transfer - Fix clipart LED preview layout and overflow issues - Fix static clipart display - Fix cache sync after save (await generateClipartCache) - Add 2-column grid layout for clipart browsing - Add localization keys for en/it/hi - Fixed TabBar length mismatch and clipart data loss on clear. - Fix save button rejecting clipart-only badges (text OR clipart now accepted) - Fix transfer mode detection for clipart (`Mode.fixed` accepted alongside `Mode.picture` — backward compatible) - Fix first-tap blank preview race condition (synchronous `renderGrid()` call ensures `_paintGrid` updates immediately)
1 parent 1ab9163 commit 3533ec0

13 files changed

Lines changed: 407 additions & 95 deletions

lib/bademagic_module/utils/file_helper.dart

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,11 @@ class FileHelper {
364364
File file = File(filePath);
365365
if (await file.exists()) {
366366
// Use share_plus to share the file
367-
final result = await Share.shareXFiles([XFile(filePath)]);
367+
final result = await SharePlus.instance.share(
368+
ShareParams(
369+
files: [XFile(filePath)],
370+
),
371+
);
368372
if (result.status == ShareResultStatus.success) {
369373
logger.i('File shared successfully');
370374
} else {
@@ -394,7 +398,92 @@ class FileHelper {
394398
}
395399
}
396400

397-
Future<bool> importBadgeData(context) async {
401+
/// Shares a clipart file (JSON) via the platform share sheet.
402+
Future<void> shareClipartData(String filename) async {
403+
try {
404+
final directory = await getApplicationDocumentsDirectory();
405+
final filePath = '${directory.path}/$filename';
406+
407+
File file = File(filePath);
408+
if (await file.exists()) {
409+
final result = await SharePlus.instance.share(
410+
ShareParams(
411+
files: [XFile(filePath)],
412+
),
413+
);
414+
if (result.status == ShareResultStatus.success) {
415+
logger.i('Clipart shared successfully');
416+
} else {
417+
logger.i('Error sharing clipart');
418+
}
419+
} else {
420+
logger.i('Clipart file not found: $filePath');
421+
}
422+
} catch (e) {
423+
logger.i('Error sharing clipart: $e');
424+
}
425+
}
426+
427+
/// Imports one or more clipart JSON files and adds them to Saved Cliparts.
428+
/// Returns true if at least one file was imported.
429+
Future<bool> importClipartData(BuildContext context) async {
430+
try {
431+
FilePickerResult? result = await FilePicker.platform.pickFiles(
432+
type: FileType.custom,
433+
allowedExtensions: ['json'],
434+
allowMultiple: true,
435+
);
436+
437+
if (result == null || result.files.isEmpty) {
438+
ToastUtils().showToast('No file selected');
439+
return false;
440+
}
441+
442+
int imported = 0;
443+
for (final platformFile in result.files) {
444+
final path = platformFile.path;
445+
if (path == null) continue;
446+
447+
File file = File(path);
448+
if (!await file.exists()) continue;
449+
450+
final content = await file.readAsString();
451+
final decoded = jsonDecode(content);
452+
453+
// Clipart format: JSON array of arrays of int (List<List<int>>)
454+
if (decoded is! List) continue;
455+
try {
456+
final list = decoded.map((row) {
457+
if (row is List) {
458+
return row.map<int>((e) => (e is int) ? e : 0).toList();
459+
}
460+
return <int>[];
461+
}).toList();
462+
if (list.isEmpty) continue;
463+
} catch (_) {
464+
continue;
465+
}
466+
467+
final filename = _generateUniqueFilename();
468+
await _writeToFile(filename, content);
469+
imported++;
470+
}
471+
472+
if (imported > 0) {
473+
await generateClipartCache();
474+
}
475+
return imported > 0;
476+
} catch (e) {
477+
if (context.mounted) {
478+
ScaffoldMessenger.of(context).showSnackBar(
479+
SnackBar(content: Text('Error importing clipart: $e')),
480+
);
481+
}
482+
return false;
483+
}
484+
}
485+
486+
Future<bool> importBadgeData(BuildContext context) async {
398487
try {
399488
// Open file picker to select a JSON file
400489
FilePickerResult? result = await FilePicker.platform.pickFiles(

lib/l10n/app_en.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,5 +211,7 @@
211211
"badgeNameHint": "Badge name",
212212
"triangle": "Triangle",
213213
"clipartSavedSuccessfully": "Clipart saved successfully",
214+
"clipartImportedSuccessfully": "Clipart imported successfully",
215+
"switchedOrientationForDraw": "Switched orientation for ease of draw",
214216
"badgeScanMode": "Badge Scan Mode"
215217
}

lib/l10n/app_hi.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@
192192
"circle": "गोला",
193193
"triangle": "तिकोना",
194194
"clipartSavedSuccessfully": "तस्वीर सेव हो गई",
195+
"clipartImportedSuccessfully": "तस्वीर अपलोड हो गई",
196+
"switchedOrientationForDraw": "ड्रॉ करने में आसानी के लिए ओरिएंटेशन बदला गया",
195197
"badgeScanMode": "बैज स्कैन मोड",
196198
"connectToBadgesWithNames": "निम्नलिखित नामों वाले बैज से कनेक्ट करें",
197199
"selectAll": "सभी चुनें",

lib/l10n/app_it.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,5 +211,7 @@
211211
"badgeNameHint": "Nome badge",
212212
"triangle": "Triangolo",
213213
"clipartSavedSuccessfully": "Clipart salvato con successo",
214+
"clipartImportedSuccessfully": "Clipart importato con successo",
215+
"switchedOrientationForDraw": "Orientamento cambiato per disegnare più comodamente",
214216
"badgeScanMode": "Modalità Scansione Badge"
215217
}

lib/providers/animation_badge_provider.dart

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,33 @@ class AnimationBadgeProvider extends ChangeNotifier {
126126
List<List<bool>> _newGrid =
127127
List.generate(11, (i) => List.generate(44, (j) => false));
128128

129+
/// When set, transfer uses this hex instead of text (for saved clipart overwrite).
130+
List<String>? _clipartHex;
131+
129132
//getter for newGrid
130133
List<List<bool>> getNewGrid() => _newGrid;
131134

135+
/// Returns stored clipart hex for transfer when in Picture mode; null otherwise.
136+
List<String>? getClipartHex() => _clipartHex;
137+
132138
//setter for newGrid
133139
void setNewGrid(List<List<bool>> grid) {
134140
_newGrid = grid;
135141
_animationIndex = 0;
136142
notifyListeners();
137143
}
138144

145+
/// Applies a saved clipart to the badge (complete overwrite, like animations).
146+
void setClipartAsBadge(List<List<int>> grid) {
147+
final boolGrid =
148+
grid.map((row) => row.map((e) => e == 1).toList()).toList();
149+
_clipartHex = Converters.convertBitmapToLEDHex(grid, false);
150+
setNewGrid(boolGrid);
151+
setAnimationMode(FixedAnimation());
152+
// Render immediately so _paintGrid is updated for display (first tap fix)
153+
renderGrid(boolGrid);
154+
}
155+
139156
Set<BadgeEffect?> get getCurrentEffect => _currentEffect;
140157

141158
/// Clears all currently active effects
@@ -198,12 +215,12 @@ class AnimationBadgeProvider extends ChangeNotifier {
198215
}
199216

200217
void setAnimationMode(BadgeAnimation? animation) {
201-
// Always reset the animation index and set the new animation
202218
_animationIndex = 0;
203219
_currentAnimation = animation ?? LeftAnimation();
204-
// Stop the timer if running
220+
if (_currentAnimation is! PictureAnimation) {
221+
_clipartHex = null;
222+
}
205223
_timer?.cancel();
206-
// Start the timer for the new animation
207224
startTimer();
208225
notifyListeners();
209226
logger.i("Animation Mode set to: $_currentAnimation and timer restarted");
@@ -227,15 +244,25 @@ class AnimationBadgeProvider extends ChangeNotifier {
227244
void badgeAnimation(
228245
String message, Converters converters, bool isInverted) async {
229246
bool isSpecial = isSpecialAnimationSelected();
230-
if (message.isEmpty && !isSpecial) {
247+
// If message is empty and no clipart is active, clear everything
248+
if (message.isEmpty && !isSpecial && _clipartHex == null) {
231249
stopAllAnimations();
232250
List<List<bool>> emptyGrid =
233251
List.generate(11, (i) => List.generate(44, (j) => false));
234252
_newGrid = emptyGrid;
235253
_paintGrid = emptyGrid;
254+
_clipartHex = null;
236255
notifyListeners();
237256
return;
238257
}
258+
// If clipart is active, don't overwrite it with empty message
259+
if (_clipartHex != null) {
260+
return;
261+
}
262+
// Only clear clipart if typing new text (non-empty message)
263+
if (message.isNotEmpty) {
264+
_clipartHex = null;
265+
}
239266
if (_timer == null || !_timer!.isActive) {
240267
startTimer();
241268
}

lib/providers/badge_message_provider.dart

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:badgemagic/bademagic_module/models/messages.dart';
1010
import 'package:badgemagic/bademagic_module/models/mode.dart';
1111
import 'package:badgemagic/bademagic_module/models/speed.dart';
1212
import 'package:badgemagic/providers/BadgeScanProvider.dart';
13+
import 'package:badgemagic/providers/animation_badge_provider.dart';
1314
import 'package:badgemagic/providers/imageprovider.dart';
1415
import 'package:badgemagic/services/localization_service.dart';
1516
import 'package:flutter/material.dart';
@@ -70,20 +71,25 @@ class BadgeMessageProvider {
7071
return data;
7172
}
7273

73-
Future<Data> generateData(
74-
String? text,
75-
bool? flash,
76-
bool? marq,
77-
bool? inverted,
78-
Speed? speed,
79-
Mode? mode,
80-
Map<String, dynamic>? jsonData) async {
74+
Future<Data> generateData(String? text, bool? flash, bool? marq,
75+
bool? inverted, Speed? speed, Mode? mode, Map<String, dynamic>? jsonData,
76+
{List<String>? clipartHex}) async {
8177
if (jsonData != null) {
8278
return fileHelper.jsonToData(jsonData);
83-
} else {
84-
return getBadgeData(text ?? '', flash ?? false, marq ?? false,
85-
speed ?? Speed.one, mode ?? Mode.left, inverted ?? false);
8679
}
80+
if (clipartHex != null && clipartHex.isNotEmpty) {
81+
return Data(messages: [
82+
Message(
83+
text: clipartHex,
84+
flash: flash ?? false,
85+
marquee: marq ?? false,
86+
speed: speed ?? Speed.one,
87+
mode: mode ?? Mode.picture,
88+
)
89+
]);
90+
}
91+
return getBadgeData(text ?? '', flash ?? false, marq ?? false,
92+
speed ?? Speed.one, mode ?? Mode.left, inverted ?? false);
8793
}
8894

8995
Future<void> transferData(
@@ -129,11 +135,9 @@ class BadgeMessageProvider {
129135
}
130136

131137
if (controllerData.getController().text.isEmpty && isSavedBadge == false) {
132-
// Allow empty text if Pacman or Fireworks mode is selected
133-
// Fireworks: Mode.fixed and animation index 19
138+
// Allow empty text if Pacman, Fireworks, or Picture mode with saved clipart
134139
bool isFireworks = false;
135140
try {
136-
// Try to get animation index from modeValueMap
137141
int fireworksIndex = 19;
138142
int cycleIndex = 20;
139143
if (mode == Mode.fixed &&
@@ -145,7 +149,12 @@ class BadgeMessageProvider {
145149
modeValueMap.containsKey(cycleIndex) &&
146150
modeValueMap[cycleIndex] == Mode.cycle) {}
147151
} catch (_) {}
148-
if (mode != Mode.pacman && !isFireworks) {
152+
final hasClipart = (mode == Mode.picture || mode == Mode.fixed) &&
153+
Provider.of<AnimationBadgeProvider>(context, listen: false)
154+
.getClipartHex()
155+
?.isNotEmpty ==
156+
true;
157+
if (mode != Mode.pacman && !isFireworks && !hasClipart) {
149158
final l10n = GetIt.instance.get<LocalizationService>().l10n;
150159
ToastUtils().showErrorToast(l10n.pleaseEnterMessage);
151160
return;
@@ -222,8 +231,16 @@ class BadgeMessageProvider {
222231
data = Data(messages: [newMessage, ...data.messages.skip(1)]);
223232
}
224233
} else {
234+
List<String>? clipartHex;
235+
if (text?.isEmpty == true &&
236+
(mode == Mode.picture || mode == Mode.fixed)) {
237+
final aniProvider =
238+
Provider.of<AnimationBadgeProvider>(context, listen: false);
239+
clipartHex = aniProvider.getClipartHex();
240+
}
225241
data = await generateData(
226-
text, flash, marq, isInverted, speedMap[speed], mode, jsonData);
242+
text, flash, marq, isInverted, speedMap[speed], mode, jsonData,
243+
clipartHex: clipartHex);
227244
}
228245

229246
DataTransferManager manager = DataTransferManager(data);

lib/view/draw_badge_screen.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,25 @@ class DrawBadge extends StatefulWidget {
3131
class _DrawBadgeState extends State<DrawBadge> {
3232
var drawToggle = DrawBadgeProvider();
3333
bool _showShapeOptions = false;
34+
bool _orientationMessageShown = false;
3435

3536
@override
3637
void didChangeDependencies() {
3738
super.didChangeDependencies();
3839
_setLandscapeOrientation();
40+
if (!_orientationMessageShown) {
41+
_orientationMessageShown = true;
42+
WidgetsBinding.instance.addPostFrameCallback((_) {
43+
if (mounted) {
44+
ToastUtils().showToast(
45+
GetIt.instance
46+
.get<LocalizationService>()
47+
.l10n
48+
.switchedOrientationForDraw,
49+
);
50+
}
51+
});
52+
}
3953
}
4054

4155
@override
@@ -253,14 +267,17 @@ class _DrawBadgeState extends State<DrawBadge> {
253267
await fileHelper.saveImage(drawToggle.getDrawViewGrid());
254268
}
255269

256-
fileHelper.generateClipartCache();
270+
await fileHelper.generateClipartCache();
257271
ToastUtils().showToast(GetIt.instance
258272
.get<LocalizationService>()
259273
.l10n
260274
.clipartSavedSuccessfully);
261275

262276
if (mounted) {
263-
Navigator.of(context).popUntil((route) => route.isFirst);
277+
Navigator.of(context).pushNamedAndRemoveUntil(
278+
'/savedClipart',
279+
(route) => false,
280+
);
264281
}
265282
},
266283
style: TextButton.styleFrom(

0 commit comments

Comments
 (0)