certificate_canvas 0.0.1
certificate_canvas: ^0.0.1 copied to clipboard
A drag-and-drop Flutter widget for designing certificates, resizing text, and overlaying images.
example/lib/main.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:certificate_canvas/certificate_canvas.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Certificate Designer',
theme: ThemeData(
primarySwatch: Colors.indigo,
scaffoldBackgroundColor: Colors.grey[100],
),
home: const DesignerPage(),
);
}
}
class DesignerPage extends StatefulWidget {
const DesignerPage({super.key});
@override
State<DesignerPage> createState() => _DesignerPageState();
}
class _DesignerPageState extends State<DesignerPage> {
Uint8List? _imageBytes; // The user's uploaded image
String? _selectedId;
final FocusNode _focusNode = FocusNode();
// --- HISTORY FOR UNDO/REDO ---
List<List<CertificateField>> _undoStack = [];
List<List<CertificateField>> _redoStack = [];
final List<Color> _colors = [
Colors.black,
Colors.white,
Colors.blue,
Colors.red,
Colors.green,
Colors.purple
];
final List<String> _fonts = ['Roboto', 'Lato', 'Oswald', 'Dancing Script'];
// Default fields to show when a user uploads a new template
List<CertificateField> _fields = [
CertificateField(
id: "1",
text: "Participant Name",
x: 0.5,
y: 0.5,
width: 300, // Added default width for resize logic
height: 80, // Added default height for resize logic
fontSize: 30,
fontName: 'Dancing Script'),
];
// --- UNDO / REDO LOGIC ---
void _saveCheckpoint() {
final deepCopy = _fields.map((f) => f.copyWith()).toList();
_undoStack.add(deepCopy);
_redoStack.clear();
if (_undoStack.length > 50) _undoStack.removeAt(0);
}
void _undo() {
if (_undoStack.isEmpty) return;
setState(() {
_redoStack.add(_fields.map((f) => f.copyWith()).toList());
_fields = _undoStack.removeLast();
_selectedId = null;
});
}
void _redo() {
if (_redoStack.isEmpty) return;
setState(() {
_undoStack.add(_fields.map((f) => f.copyWith()).toList());
_fields = _redoStack.removeLast();
});
}
// --- ACTIONS ---
Future<void> _pickImage() async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
final Uint8List bytes = await image.readAsBytes();
setState(() {
_imageBytes = bytes;
});
}
}
void _handleAddText() {
_saveCheckpoint();
setState(() {
_fields.add(CertificateField(
id: DateTime.now().toString(),
text: "New Text",
));
});
}
void _handleEditText(String id) {
final field = _fields.firstWhere((f) => f.id == id);
TextEditingController controller = TextEditingController(text: field.text);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Edit Text"),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(border: OutlineInputBorder()),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Cancel")),
ElevatedButton(
onPressed: () {
_saveCheckpoint();
setState(() {
final index = _fields.indexWhere((f) => f.id == id);
if (index != -1)
_fields[index] =
_fields[index].copyWith(text: controller.text);
});
Navigator.pop(context);
},
child: const Text("Save"),
)
],
),
);
}
// --- UPDATED: HANDLES STYLING ---
void _updateSelectedField(
{double? fontSize,
Color? color,
String? fontName,
bool? isBold, // NEW
bool? isItalic // NEW
}) {
if (_selectedId == null) return;
_saveCheckpoint();
setState(() {
final index = _fields.indexWhere((f) => f.id == _selectedId);
if (index != -1) {
_fields[index] = _fields[index].copyWith(
fontSize: fontSize,
color: color,
fontName: fontName,
isBold: isBold, // NEW
isItalic: isItalic // NEW
);
}
});
}
void _deleteSelected() {
if (_selectedId == null) return;
_saveCheckpoint();
setState(() {
_fields.removeWhere((f) => f.id == _selectedId);
_selectedId = null;
});
}
@override
Widget build(BuildContext context) {
if (_imageBytes == null) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.cloud_upload_outlined,
size: 80, color: Colors.indigo),
const SizedBox(height: 20),
const Text("Upload Certificate Template",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
const Text(
"Upload a blank certificate image (JPG/PNG) to start designing.",
style: TextStyle(color: Colors.grey)),
const SizedBox(height: 30),
ElevatedButton.icon(
icon: const Icon(Icons.add_photo_alternate),
label: const Text("Choose Image"),
style: ElevatedButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
textStyle: const TextStyle(fontSize: 18),
),
onPressed: _pickImage,
),
],
),
),
);
}
CertificateField? selectedField;
if (_selectedId != null) {
try {
selectedField = _fields.firstWhere((f) => f.id == _selectedId);
} catch (e) {
_selectedId = null;
}
}
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.delete): _deleteSelected,
const SingleActivator(LogicalKeyboardKey.backspace): _deleteSelected,
const SingleActivator(LogicalKeyboardKey.keyZ, control: true): _undo,
const SingleActivator(LogicalKeyboardKey.keyY, control: true): _redo,
},
child: Focus(
focusNode: _focusNode,
autofocus: true,
child: Scaffold(
body: Column(
children: [
// HEADER
Container(
height: 60,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Text("CertBuilder",
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(width: 20),
TextButton.icon(
icon: const Icon(Icons.image, size: 16),
label: const Text("Change Template"),
onPressed: _pickImage,
),
const VerticalDivider(indent: 10, endIndent: 10),
IconButton(
icon: const Icon(Icons.undo),
onPressed: _undoStack.isNotEmpty ? _undo : null,
tooltip: "Undo (Ctrl+Z)",
),
IconButton(
icon: const Icon(Icons.redo),
onPressed: _redoStack.isNotEmpty ? _redo : null,
tooltip: "Redo (Ctrl+Y)",
),
const Spacer(),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text("Add Text"),
onPressed: _handleAddText,
),
],
),
),
// --- UPDATED TOOLBAR ---
if (selectedField != null)
Container(
height: 50,
color: Colors.white,
// Changed to ListView to prevent overflow with new buttons
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 10),
children: [
DropdownButton<String>(
value: _fonts.contains(selectedField.fontName)
? selectedField.fontName
: 'Roboto',
items: _fonts
.map((f) =>
DropdownMenuItem(value: f, child: Text(f)))
.toList(),
onChanged: (val) {
if (val != null) _updateSelectedField(fontName: val);
},
underline: Container(),
),
const VerticalDivider(),
// --- NEW BOLD & ITALIC BUTTONS ---
IconButton(
tooltip: "Bold",
icon: Icon(Icons.format_bold,
color: selectedField.isBold
? Colors.black
: Colors.grey),
onPressed: () => _updateSelectedField(
isBold: !selectedField!.isBold),
),
IconButton(
tooltip: "Italic",
icon: Icon(Icons.format_italic,
color: selectedField.isItalic
? Colors.black
: Colors.grey),
onPressed: () => _updateSelectedField(
isItalic: !selectedField!.isItalic),
),
const VerticalDivider(),
// Colors
..._colors.map((c) => GestureDetector(
onTap: () => _updateSelectedField(color: c),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: 20,
height: 20,
decoration: BoxDecoration(
color: c,
shape: BoxShape.circle,
border:
Border.all(color: Colors.grey.shade300)),
),
)),
const VerticalDivider(),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: _deleteSelected),
],
),
),
// CANVAS
Expanded(
child: GestureDetector(
onTap: () {
setState(() => _selectedId = null);
_focusNode.requestFocus();
},
child: Container(
color: Colors.grey[100],
child: Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
spreadRadius: 2)
],
),
child: CertificateCanvas(
imageBytes: _imageBytes,
fields: _fields,
selectedFieldId: _selectedId,
onDragStart: _saveCheckpoint,
onFieldTap: (id) {
setState(() => _selectedId = id);
_focusNode.requestFocus();
},
onFieldDoubleTap: _handleEditText,
onFieldUpdate: (id, updatedField) {
setState(() {
final index =
_fields.indexWhere((f) => f.id == id);
if (index != -1) _fields[index] = updatedField;
});
},
),
),
),
),
),
),
),
],
),
),
),
);
}
}