Bundle Noto Sans Arabic + Noto Sans JP (SIL OFL 1.1) as per-glyph fallbacks for both the UI text theme and every generated PDF, so RTL (Arabic) and CJK render on all platforms — including desktop, where the system font may lack them — instead of tofu. A shared pdf_fonts.dart centralizes the PDF theme and the three label/catalog/recovery services reuse it. Add Japanese (ja) as the CJK reference locale — fittingly, the app's own name is Japanese (種, tane, 'seed'). Core strings translated; the rest fall back to English via slang. Wired into supportedLocales (automatic) and the settings picker. Fix three physical Alignment.centerLeft -> AlignmentDirectional.centerStart in variety_detail_screen, and make month-abbreviation and avatar-initial truncation grapheme-safe (.characters) so CJK text is never cut mid-character. Tests: CJK/Arabic PDF glyph render, ja locale resolution + English fallback. The chat-bubble mirroring flagged by the audit was verified correct (already respects Directionality) and left unchanged.
75 lines
2.3 KiB
Dart
75 lines
2.3 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:pdf/pdf.dart';
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
|
|
import 'file_service.dart';
|
|
import 'pdf_fonts.dart';
|
|
|
|
/// Renders the printable recovery sheet — a QR plus the typed code and short
|
|
/// instructions ("keep two paper copies, like your best seed"). All strings
|
|
/// arrive already localized.
|
|
class RecoverySheetService {
|
|
RecoverySheetService({required FileService files}) : _files = files;
|
|
|
|
final FileService _files;
|
|
|
|
Future<Uint8List> buildPdf({
|
|
required String title,
|
|
required String intro,
|
|
required String code,
|
|
}) async {
|
|
final fonts = await loadPdfFonts();
|
|
final doc = pw.Document(theme: fonts.theme);
|
|
doc.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4,
|
|
build: (context) => pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text(title, style: pw.TextStyle(font: fonts.bold, fontSize: 20)),
|
|
pw.SizedBox(height: 12),
|
|
pw.Text(intro, style: const pw.TextStyle(fontSize: 11)),
|
|
pw.SizedBox(height: 24),
|
|
pw.Center(
|
|
child: pw.BarcodeWidget(
|
|
barcode: pw.Barcode.qrCode(),
|
|
data: code,
|
|
width: 220,
|
|
height: 220,
|
|
// The code is printed below with the Unicode-capable face;
|
|
// the widget's built-in caption would fall back to Courier.
|
|
drawText: false,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 24),
|
|
pw.Center(
|
|
child: pw.Text(
|
|
code,
|
|
textAlign: pw.TextAlign.center,
|
|
style: pw.TextStyle(font: fonts.bold, fontSize: 12),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
return doc.save();
|
|
}
|
|
|
|
/// Builds the sheet and asks the user where to keep it. Returns true when
|
|
/// saved, false when they cancelled.
|
|
Future<bool> saveSheet({
|
|
required String title,
|
|
required String intro,
|
|
required String code,
|
|
required String suggestedName,
|
|
}) async {
|
|
final bytes = await buildPdf(title: title, intro: intro, code: code);
|
|
final path = await _files.saveFile(
|
|
suggestedName: suggestedName,
|
|
bytes: bytes,
|
|
);
|
|
return path != null;
|
|
}
|
|
}
|