Skip to content

Commit 40a3afb

Browse files
committed
feat: improve coverage and resolve problems to generate the img
1 parent 2b5551f commit 40a3afb

16 files changed

Lines changed: 427 additions & 61 deletions

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Pictograms are universally easy to identify, making communication simple and int
3636

3737
- **Pictogram keyboard** with categorized icons (descriptive, people, prepositions, determiners, nouns, verbs)
3838
- **Visual board** to compose messages by selecting pictograms
39-
- **Share** the board as an image with text description
39+
- **Share** the board as a 1080×1080 image with text description
4040
- **Installable PWA** — add to home screen on Android and iOS
4141
- **Multi-language support**: English, Spanish, Catalan, Basque, French, Galician, Portuguese, Valencian
4242
- **Accessibility-focused** design with semantic labels for screen readers
@@ -73,18 +73,19 @@ lib/
7373
├── main.dart # App entry point
7474
├── data/
7575
│ └── pictogram_data.dart # Pictogram categories and icon data
76-
├── utils/
77-
│ └── pictogram_utils.dart # Icon utility functions
7876
├── screens/
7977
│ └── pictotap_screen.dart # Main screen with board and state
78+
├── services/
79+
│ ├── image_saver.dart # Platform export selector
80+
│ ├── image_saver_native.dart # Native image sharing (Android/iOS)
81+
│ ├── image_saver_web.dart # Web image download / Web Share API
82+
│ └── image_saver_stub.dart # Stub for unsupported platforms
83+
├── utils/
84+
│ └── pictogram_utils.dart # Icon utility functions
8085
├── widgets/
86+
│ ├── board_empty_hint.dart # Empty board hint animation
8187
│ ├── pictogram_icon.dart # Pictogram icon widget
82-
│ ├── pictogram_keyboard.dart # Keyboard widget with categories
83-
│ └── board_empty_hint.dart # Empty board hint animation
84-
├── image_saver.dart # Platform export selector
85-
├── image_saver_native.dart # Native image sharing
86-
├── image_saver_web.dart # Web image download
87-
├── image_saver_stub.dart # Stub for unsupported platforms
88+
│ └── pictogram_keyboard.dart # Keyboard widget with categories
8889
└── l10n/ # Localization (ARB files)
8990
```
9091

lib/data/pictogram_data.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class KeyboardCategory {
99
}
1010

1111
const String spaceIcon = '__space__';
12-
const int defaultBoardMaxIcons = 10;
12+
const int defaultBoardMaxIcons = 4;
1313
const int maxRecommendationIcons = 6;
1414

1515
const List<String> descriptiveIcons = [

lib/screens/pictotap_screen.dart

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'dart:async';
2+
import 'dart:math' as math;
3+
import 'dart:typed_data';
24
import 'dart:ui' as ui;
35
import 'package:flutter/material.dart';
46
import 'package:flutter/rendering.dart';
57
import 'package:pictotap/data/pictogram_data.dart';
6-
import 'package:pictotap/image_saver.dart';
8+
import 'package:pictotap/services/image_saver.dart';
79
import 'package:pictotap/l10n/app_localizations.dart';
810
import 'package:pictotap/utils/pictogram_utils.dart';
911
import 'package:pictotap/widgets/board_empty_hint.dart';
@@ -28,7 +30,9 @@ class _PictoTapScreenState extends State<PictoTapScreen> {
2830

2931
static const String _backgroundAssetPath =
3032
'assets/background/background.webp';
31-
static const double _boardIconSize = 80;
33+
static const double _boardIconSize = 110;
34+
static const int _shareImageSize = 1080;
35+
static const Color _backgroundFallback = Color(0xFFF5F0EB);
3236
static const Duration _animationDuration = Duration(milliseconds: 450);
3337

3438
void _addIcon(String icon) {
@@ -110,11 +114,51 @@ class _PictoTapScreenState extends State<PictoTapScreen> {
110114
as RenderRepaintBoundary?;
111115
if (boundary == null) return;
112116

113-
final image = await boundary.toImage(pixelRatio: 3);
114-
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
115-
if (byteData == null) return;
117+
const int out = _shareImageSize;
118+
final bSize = boundary.size;
119+
final pixelRatio = out / math.min(bSize.width, bSize.height);
120+
final rawImage = await boundary.toImage(pixelRatio: pixelRatio);
116121

117-
final pngBytes = byteData.buffer.asUint8List();
122+
final srcW = rawImage.width;
123+
final srcH = rawImage.height;
124+
final rawBytes = await rawImage.toByteData(
125+
format: ui.ImageByteFormat.rawStraightRgba,
126+
);
127+
if (rawBytes == null) return;
128+
final srcPixels = rawBytes.buffer.asUint8List();
129+
130+
final cropSize = math.min(srcW, srcH);
131+
final cropX = (srcW - cropSize) ~/ 2;
132+
final cropY = (srcH - cropSize) ~/ 2;
133+
134+
final outPixels = Uint8List(out * out * 4);
135+
for (var y = 0; y < out; y++) {
136+
final sy = (cropY + y * cropSize ~/ out).clamp(0, srcH - 1);
137+
for (var x = 0; x < out; x++) {
138+
final sx = (cropX + x * cropSize ~/ out).clamp(0, srcW - 1);
139+
final si = (sy * srcW + sx) * 4;
140+
final di = (y * out + x) * 4;
141+
outPixels[di] = srcPixels[si];
142+
outPixels[di + 1] = srcPixels[si + 1];
143+
outPixels[di + 2] = srcPixels[si + 2];
144+
outPixels[di + 3] = srcPixels[si + 3];
145+
}
146+
}
147+
148+
final completer = Completer<ui.Image>();
149+
ui.decodeImageFromPixels(
150+
outPixels,
151+
out,
152+
out,
153+
ui.PixelFormat.rgba8888,
154+
completer.complete,
155+
);
156+
final outputImage = await completer.future;
157+
final pngData =
158+
await outputImage.toByteData(format: ui.ImageByteFormat.png);
159+
if (pngData == null) return;
160+
161+
final pngBytes = pngData.buffer.asUint8List();
118162
final fileName =
119163
'pictotap-board-${DateTime.now().millisecondsSinceEpoch}.png';
120164
await saveAndShareImage(pngBytes, fileName, shareText);
@@ -223,30 +267,29 @@ class _PictoTapScreenState extends State<PictoTapScreen> {
223267
),
224268
foregroundColor: Colors.white,
225269
),
226-
body: Stack(
270+
body: Column(
227271
children: [
228-
Positioned.fill(
229-
child: Image.asset(
230-
_backgroundAssetPath,
231-
fit: BoxFit.cover,
232-
errorBuilder: (context, error, stackTrace) {
233-
return Container(color: const Color(0xFFF5F0EB));
234-
},
235-
),
236-
),
237-
Positioned.fill(
238-
child: Container(color: Colors.white.withAlpha(140)),
239-
),
240-
Column(
241-
children: [
242-
Expanded(
243-
child: GestureDetector(
244-
onTap: _toggleKeyboard,
245-
child: RepaintBoundary(
246-
key: _boardBoundaryKey,
247-
child: Container(
272+
Expanded(
273+
child: GestureDetector(
274+
onTap: _toggleKeyboard,
275+
child: RepaintBoundary(
276+
key: _boardBoundaryKey,
277+
child: Stack(
278+
children: [
279+
Positioned.fill(
280+
child: Image.asset(
281+
_backgroundAssetPath,
282+
fit: BoxFit.cover,
283+
errorBuilder: (context, error, stackTrace) {
284+
return Container(color: _backgroundFallback);
285+
},
286+
),
287+
),
288+
Positioned.fill(
289+
child: Container(color: Colors.white.withAlpha(140)),
290+
),
291+
SizedBox(
248292
width: double.infinity,
249-
color: Colors.transparent,
250293
child: _selectedIcons.isEmpty
251294
? Center(
252295
child: BoardEmptyHint(
@@ -267,19 +310,19 @@ class _PictoTapScreenState extends State<PictoTapScreen> {
267310
),
268311
),
269312
),
270-
),
313+
],
271314
),
272315
),
273-
if (_isKeyboardVisible)
274-
PictogramKeyboard(
275-
onIconSelected: _addIcon,
276-
onSpace: _addSpace,
277-
onBackspace: _removeLast,
278-
recommendations: buildRecommendations(_selectedIcons),
279-
showLimitBanner: _showLimitReachedBanner,
280-
),
281-
],
316+
),
282317
),
318+
if (_isKeyboardVisible)
319+
PictogramKeyboard(
320+
onIconSelected: _addIcon,
321+
onSpace: _addSpace,
322+
onBackspace: _removeLast,
323+
recommendations: buildRecommendations(_selectedIcons),
324+
showLimitBanner: _showLimitReachedBanner,
325+
),
283326
],
284327
),
285328
floatingActionButton: _isKeyboardVisible

test/data/pictogram_data_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:pictotap/data/pictogram_data.dart';
3+
4+
void main() {
5+
test('all icon lists use correct prefix and have no duplicates', () {
6+
final expected = {
7+
'descriptive:': descriptiveIcons,
8+
'people:': peopleIcons,
9+
'prepositions:': prepositionsIcons,
10+
'some:': someIcons,
11+
'substantive:': substantiveIcons,
12+
'verbs:': verbsIcons,
13+
};
14+
15+
final allIcons = <String>[];
16+
for (final entry in expected.entries) {
17+
expect(entry.value, isNotEmpty, reason: '${entry.key} list is empty');
18+
for (final icon in entry.value) {
19+
expect(icon, startsWith(entry.key));
20+
}
21+
allIcons.addAll(entry.value);
22+
}
23+
24+
expect(allIcons.toSet().length, allIcons.length,
25+
reason: 'Found duplicate icons');
26+
});
27+
28+
test('keyboardCategories maps all icon lists', () {
29+
expect(keyboardCategories.length, 6);
30+
expect(keyboardCategories[0].icons, descriptiveIcons);
31+
expect(keyboardCategories[5].icons, verbsIcons);
32+
});
33+
}

test/helpers/pump_app.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:pictotap/l10n/app_localizations.dart';
4+
5+
extension PumpApp on WidgetTester {
6+
Future<void> pumpApp(Widget widget) async {
7+
await pumpWidget(
8+
MaterialApp(
9+
localizationsDelegates: AppLocalizations.localizationsDelegates,
10+
supportedLocales: AppLocalizations.supportedLocales,
11+
locale: const Locale('en'),
12+
home: Scaffold(body: widget),
13+
),
14+
);
15+
await pump();
16+
}
17+
}

test/image_saver_stub_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import 'dart:typed_data';
2+
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:pictotap/services/image_saver_stub.dart';
5+
6+
void main() {
7+
test('saveAndShareImage throws UnsupportedError', () {
8+
expect(
9+
() => saveAndShareImage(Uint8List(0), 'test.png', 'text'),
10+
throwsA(isA<UnsupportedError>()),
11+
);
12+
});
13+
}

0 commit comments

Comments
 (0)