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,116 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:photobooth_mobile_app/core/camera/camera_service.dart';
import 'package:photobooth_mobile_app/modules/camera/outside_dim_mask.dart';
import 'package:photobooth_mobile_app/modules/preview/preview_screen.dart';
class CameraCaptureScreen extends StatefulWidget {
final String frameAsset;
const CameraCaptureScreen({super.key, required this.frameAsset});
@override
State<CameraCaptureScreen> createState() => _CameraCaptureScreenState();
}
class _CameraCaptureScreenState extends State<CameraCaptureScreen> {
final CameraService _cameraService = CameraService();
bool _ready = false;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
await _cameraService.init();
setState(() => _ready = true);
}
@override
void dispose() {
_cameraService.dispose();
super.dispose();
}
Future<void> _takePicture() async {
final XFile raw = await _cameraService.takePicture();
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (_) => PreviewScreen(
imagePath: raw.path,
frameAsset: widget.frameAsset,
isFrontCamera: _cameraService.isFront,
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: !_ready
? const Center(child: CupertinoActivityIndicator())
: Column(
children: [
Stack(
children: [
CameraPreview(_cameraService.controller!),
const Positioned.fill(child: OutsideDimMask(opacity: 1)),
Positioned.fill(
child: IgnorePointer(
child: AspectRatio(
aspectRatio: 148 / 210,
child: Image.asset(
widget.frameAsset,
fit: BoxFit.contain,
),
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: Icon(
Icons.cameraswitch,
color: Colors.white,
size: 28,
),
onPressed: () async {
await _cameraService.switchCamera();
setState(() {});
},
),
GestureDetector(
onTap: _takePicture,
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.close, color: Colors.white, size: 28),
),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
class OutsideDimMask extends StatelessWidget {
final double opacity;
const OutsideDimMask({super.key, this.opacity = 0.6});
@override
Widget build(BuildContext context) {
const a5Ratio = 148 / 210;
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final height = constraints.maxHeight;
double frameWidth;
double frameHeight;
if (width / height > a5Ratio) {
frameHeight = height;
frameWidth = height * a5Ratio;
} else {
frameWidth = width;
frameHeight = width / a5Ratio;
}
final left = (width - frameWidth) / 2;
final top = (height - frameHeight) / 2;
return Stack(
children: [
// Top
Positioned(
left: 0,
right: 0,
top: 0,
height: top,
child: Container(color: Colors.black.withOpacity(opacity)),
),
// Bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
height: top,
child: Container(color: Colors.black.withOpacity(opacity)),
),
// Left
Positioned(
left: 0,
top: top,
width: left,
height: frameHeight,
child: Container(color: Colors.black.withOpacity(opacity)),
),
// Right
Positioned(
right: 0,
top: top,
width: left,
height: frameHeight,
child: Container(color: Colors.black.withOpacity(opacity)),
),
],
);
},
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photobooth_mobile_app/modules/camera/camera_screen.dart';
class FrameSelectionScreen extends StatefulWidget {
const FrameSelectionScreen({super.key});
@override
_FrameSelectionScreenState createState() => _FrameSelectionScreenState();
}
class _FrameSelectionScreenState extends State<FrameSelectionScreen> {
String? selectedFrameAsset;
final frames = [
{
'asset': 'assets/images/nurtech_twibbon.png',
'sample': 'assets/images/nurtech_sample.png',
'label': 'Nurtech School',
},
{
'asset': 'assets/images/nuruliman_twibbon.png',
'sample': 'assets/images/nuruliman_sample.png',
'label': 'Nurul Iman',
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Pilih Frame')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Column(
children: frames.map((f) {
final asset = f['asset']!;
final sample = f['sample']!;
final label = f['label']!;
final isSelected = selectedFrameAsset == asset;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => selectedFrameAsset = asset),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
margin: EdgeInsets.all(8),
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(color: Colors.black12, blurRadius: 6),
],
border: isSelected
? Border.all(color: Colors.deepOrange, width: 2)
: null,
),
child: Column(
children: [
Expanded(
child: Image.asset(sample, fit: BoxFit.contain),
),
SizedBox(height: 8),
Text(label, textAlign: TextAlign.center),
],
),
),
),
);
}).toList(),
),
),
SizedBox(height: 12),
ElevatedButton(
onPressed: selectedFrameAsset == null
? null
: () => Navigator.push(
context,
CupertinoPageRoute(
builder: (_) => CameraCaptureScreen(
frameAsset: selectedFrameAsset!,
),
),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Text('Lanjutkan', style: TextStyle(fontSize: 16)),
),
),
SizedBox(height: 12),
],
),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
class OpacityDialog extends StatefulWidget {
final double initial;
const OpacityDialog({super.key, required this.initial});
@override
_OpacityDialogState createState() => _OpacityDialogState();
}
class _OpacityDialogState extends State<OpacityDialog> {
late double val;
@override
void initState() {
super.initState();
val = widget.initial;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Opacity Watermark'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Slider(
value: val,
min: 0.1,
max: 1.0,
divisions: 9,
label: '${(val * 100).toStringAsFixed(0)}%',
onChanged: (v) => setState(() => val = v),
),
SizedBox(height: 8),
Text('Preview: ${(val * 100).toStringAsFixed(0)}%'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Batal'),
),
TextButton(
onPressed: () => Navigator.pop(context, val),
child: Text('OK'),
),
],
);
}
}

View File

@@ -0,0 +1,103 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photobooth_mobile_app/core/image/image_cleanup_service.dart';
import 'package:photobooth_mobile_app/core/image/image_compositor.dart';
import 'package:photobooth_mobile_app/core/print/print_service.dart';
class PreviewScreen extends StatefulWidget {
final String imagePath;
final String frameAsset;
final bool isFrontCamera;
const PreviewScreen({
super.key,
required this.imagePath,
required this.frameAsset,
required this.isFrontCamera,
});
@override
State<PreviewScreen> createState() => _PreviewScreenState();
}
class _PreviewScreenState extends State<PreviewScreen> {
final ImageCompositor _compositor = ImageCompositor();
final PrintService _printer = PrintService();
final ImageCleanupService _cleanup = ImageCleanupService();
late String _finalPath;
String? _lastFinalPath;
bool _loading = true;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
final dir = await getTemporaryDirectory();
final twibbonData = await rootBundle.load(widget.frameAsset);
await _cleanup.deleteIfExists(_lastFinalPath);
final newPath = await _compositor.composeFinalA5(
imagePath: widget.imagePath,
twibbonBytes: twibbonData.buffer.asUint8List(),
tempDir: dir.path,
isFrontCamera: widget.isFrontCamera,
);
_lastFinalPath = newPath;
_finalPath = newPath;
await _cleanup.keepLastN(tempDir: dir.path, maxFiles: 10);
if (mounted) {
setState(() => _loading = false);
}
}
@override
void dispose() {
_cleanup.deleteIfExists(_finalPath);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pratinjau')),
body: Column(
children: [
Expanded(
child: _loading
? const Center(child: CupertinoActivityIndicator())
: Center(
child: AspectRatio(
aspectRatio: 148 / 210,
child: Image.file(File(_finalPath), fit: BoxFit.contain),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
icon: const Icon(Icons.print),
label: Text(_loading ? 'Menyiapkan...' : 'Cetak'),
onPressed: _loading
? null
: () async {
final pdfPath = await _printer.generateA5Pdf(_finalPath);
await _printer.sharePdf(pdfPath);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photobooth_mobile_app/modules/frame/select_frame_screen.dart';
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlutterLogo(size: 96),
SizedBox(height: 24),
Text(
'Photo Frame',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 12),
Text(
'Pilih frame, ambil foto, cetak A5',
textAlign: TextAlign.center,
),
SizedBox(height: 36),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => const FrameSelectionScreen(),
),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Text('Mulai', style: TextStyle(fontSize: 16)),
),
),
],
),
),
),
);
}
}