first commit

This commit is contained in:
2026-01-20 16:34:54 +07:00
commit 2ac772b6de
4186 changed files with 123824 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import 'package:camera/camera.dart';
class CameraService {
CameraController? controller;
late List<CameraDescription> _cameras;
bool _isFront = true;
bool get isFront => _isFront;
/// Init default
Future<void> init() async {
_cameras = await availableCameras();
final front = _cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.front,
orElse: () => _cameras.first,
);
controller = CameraController(
front,
ResolutionPreset.high,
enableAudio: false,
);
await controller!.initialize();
_isFront = true;
}
/// Switch front <-> back
Future<void> switchCamera() async {
if (_cameras.length < 2) return;
final newCam = _cameras.firstWhere(
(c) =>
c.lensDirection ==
(_isFront ? CameraLensDirection.back : CameraLensDirection.front),
);
await controller?.dispose();
controller = CameraController(
newCam,
ResolutionPreset.high,
enableAudio: false,
);
await controller!.initialize();
_isFront = !_isFront;
}
Future<XFile> takePicture() async {
if (controller == null || !controller!.value.isInitialized) {
throw Exception('Camera not ready');
}
return controller!.takePicture();
}
void dispose() {
controller?.dispose();
}
}

View File

@@ -0,0 +1,54 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class ImageCleanupService {
/// Hapus SEMUA file sementara
Future<void> cleanAllTemp(String tempDir) async {
final dir = Directory(tempDir);
if (!await dir.exists()) return;
for (final file in dir.listSync()) {
if (file is File) {
await file.delete();
}
}
}
/// Simpan maksimal N foto terakhir
Future<void> keepLastN({required String tempDir, int maxFiles = 10}) async {
final dir = Directory(tempDir);
if (!await dir.exists()) return;
final files =
dir
.listSync()
.whereType<File>()
.where((f) => f.path.contains('final_'))
.toList()
..sort(
(a, b) => a.statSync().modified.compareTo(b.statSync().modified),
);
if (files.length <= maxFiles) return;
for (final file in files.take(files.length - maxFiles)) {
await file.delete();
}
}
/// Hapus file tertentu (aman)
Future<void> deleteIfExists(String? path) async {
if (path == null) return;
final file = File(path);
if (await file.exists()) {
await file.delete();
}
}
}
Future<void> cleanTempOnStart() async {
final dir = await getTemporaryDirectory();
final cleanup = ImageCleanupService();
await cleanup.cleanAllTemp(dir.path);
}

View File

@@ -0,0 +1,83 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;
/// PUBLIC API
class ImageCompositor {
Future<String> composeFinalA5({
required String imagePath,
required Uint8List twibbonBytes,
required String tempDir,
required bool isFrontCamera,
}) {
return compute(_finalIsolate, {
'imagePath': imagePath,
'twibbonBytes': twibbonBytes,
'tempDir': tempDir,
'isFrontCamera': isFrontCamera,
});
}
}
Future<String> _finalIsolate(Map<String, dynamic> args) async {
const int a5Width = 1748;
const int a5Height = 2480;
final String imagePath = args['imagePath'];
final Uint8List twibbonBytes = args['twibbonBytes'];
final String tempDir = args['tempDir'];
final bool isFrontCamera = args['isFrontCamera'];
// 1⃣ Decode foto kamera
final raw = img.decodeImage(await File(imagePath).readAsBytes());
if (raw == null) throw Exception('Invalid photo');
// 2⃣ Samakan dengan preview kamera depan
if (isFrontCamera) {
img.flipHorizontal(raw);
}
// 3⃣ Crop ke rasio A5
final cropped = _cropToA5(raw);
// 4⃣ Resize ke resolusi CETAK A5
final photoA5 = img.copyResize(
cropped,
width: a5Width,
height: a5Height,
interpolation: img.Interpolation.average,
);
// 5⃣ Decode & resize twibbon SEKALI
final twibbon = img.decodeImage(twibbonBytes)!;
final twibbonA5 = img.copyResize(twibbon, width: a5Width, height: a5Height);
// 6⃣ Composite
img.compositeImage(photoA5, twibbonA5);
// 7⃣ Encode JPEG (print-ready)
final jpgBytes = img.encodeJpg(photoA5, quality: 90);
// 8⃣ Simpan FINAL FILE
final file = File(
'$tempDir/final_${DateTime.now().millisecondsSinceEpoch}.jpg',
);
await file.writeAsBytes(jpgBytes);
return file.path;
}
img.Image _cropToA5(img.Image src) {
const double a5Ratio = 148 / 210;
final srcRatio = src.width / src.height;
if (srcRatio > a5Ratio) {
final newWidth = (src.height * a5Ratio).round();
final x = ((src.width - newWidth) / 2).round();
return img.copyCrop(src, x: x, y: 0, width: newWidth, height: src.height);
} else {
final newHeight = (src.width / a5Ratio).round();
final y = ((src.height - newHeight) / 2).round();
return img.copyCrop(src, x: 0, y: y, width: src.width, height: newHeight);
}
}

View File

@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:pdf/pdf.dart';
import 'package:printing/printing.dart';
class PrintService {
Future<String> generateA5Pdf(String imagePath) async {
final bytes = await File(imagePath).readAsBytes();
final image = pw.MemoryImage(bytes);
final doc = pw.Document();
doc.addPage(
pw.Page(
pageFormat: PdfPageFormat.a5,
margin: pw.EdgeInsets.zero, // ⬅️ NO PADDING
build: (_) => pw.Image(
image,
fit: pw.BoxFit.cover, // ⬅️ FULL PAGE
),
),
);
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/photo_a5.pdf');
await file.writeAsBytes(await doc.save());
return file.path;
}
/// Share PDF ke app printer (USB / Wi-Fi)
Future<void> sharePdf(String pdfPath) async {
final bytes = await File(pdfPath).readAsBytes();
await Printing.sharePdf(bytes: bytes, filename: 'photobooth_a5.pdf');
}
}