inspection_camera 0.1.0
inspection_camera: ^0.1.0 copied to clipboard
A new Flutter RzCamera Package.
example/lib/main.dart
import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:inspection_camera/inspection_camera.dart';
void main() {
try {
runApp(const MyApp());
} catch (e) {
print(e);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Camera POC'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<InspectionMedia?> images = [];
final TextEditingController textEditingController =
TextEditingController(text: "3");
bool loading = false;
Future<T> loadingWrapper<T>(Future<T> Function() callback) async {
setState(() {
loading = true;
});
try {
return await callback.call();
} finally {
setState(() {
loading = false;
});
}
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: ListView(
children: [
if (!loading) ...[
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Take single picture'),
onPressed: () async {
loadingWrapper(() async {
final image = await CameraModule(
navigationHandler:
NavigationHandler.defaultNavigator(context),
defaultCaptureConfig: ImageCaptureConfig(
cameraModuleCallbacks: ImageCaptureCallbacks(
onCameraLoaded: () {},
onRotationPrompt: (a) {},
onFlashToggle: (a) {},
onCapture: () {},
onPreviewLoad: () {},
onPreviewAccepted: () {},
onPreviewRejected: () {},
onRetakeClick: () async {
bool valueToReturn = false;
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text(
'Current image will be discarded.',
),
actions: [
InkWell(
onTap: () {
Navigator.pop(context);
valueToReturn = true;
},
child: const Text('ok')),
InkWell(
onTap: () {
Navigator.pop(context);
valueToReturn = false;
},
child: const Text('cancel')),
],
);
});
return valueToReturn;
}),
)).capture(
captureConfig: (defaultConfig) {
return defaultConfig;
},
);
if (image != null) {
setState(() {
images.insert(0, image);
});
}
});
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Take single video'),
onPressed: () async {
loadingWrapper(() async {
final image = await CameraModule(
navigationHandler:
NavigationHandler.defaultNavigator(context),
).capture(
captureConfig: (defaultConfig) => VideoCaptureConfig(),
);
if (image != null) {
setState(() {
images.insert(0, image);
});
}
});
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Flexible(
child: TextFormField(
controller: textEditingController,
onChanged: (value) {
if (int.tryParse(value) == null && value.isNotEmpty) {
textEditingController.text = "1";
}
setState(() {});
},
),
),
Expanded(
child: FilledButton(
child: Text('Take ${int.tryParse(
textEditingController.text,
) ?? 1} pictures'),
onPressed: () async {
loadingWrapper(() async {
await CameraModule(
navigationHandler:
NavigationHandler.defaultNavigator(context),
defaultCaptureConfig: ImageCaptureConfig(
captureWidgetBuilders: CaptureWidgetBuilders(
shutterIconBuilder: (context) =>
const Placeholder(),
)),
).captureMultiple(
captureConfigs: (defaultConfig) {
final List<MediaCaptureConfig> configs = [];
for (var i = 0;
i < int.parse(textEditingController.text);
i++) {
configs.add(defaultConfig);
/*if (i % 2 != 0) {
configs Fs.add(VideoCaptureConfig());
} else {
configs.add(defaultConfig);
}*/
}
return configs;
},
onPictureTaken: (index, captureConfig, data) {
images.insert(0, data);
setState(() {});
},
);
});
},
),
),
],
),
),
] else
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
],
),
const Divider(),
...images.map(
(e) => e == null
? Container()
: switch (e) {
InspectionImage() => buildImage(context, e),
InspectionVideo() => Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: buildStackVid(context, e.toWidget(), e.data),
),
),
},
),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
SizedBox buildImage(BuildContext context, InspectionImage e) {
return SizedBox(
width: MediaQuery.of(context).size.width,
child: Card(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FutureBuilder(
future: e.data.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return buildStack(
context,
e.toWidget(),
snapshot.data!,
);
} else {
return Container();
}
},
),
FutureBuilder(
future: e.data.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return buildStack(
context,
e.toWidget(),
snapshot.data!,
);
} else {
return Container();
}
},
),
],
),
FilledButton(
onPressed: () async {
if (kIsWeb) {
await FileSaver.instance.saveFile(
name: 'originalImage.png',
filePath: e.data.path,
);
await Future.delayed(
const Duration(seconds: 1),
);
await FileSaver.instance.saveFile(
name: 'editedImage.png',
bytes: await e.data.readAsBytes(),
);
} else {
await ImageGallerySaver.saveFile(
e.data.path,
);
await ImageGallerySaver.saveFile(e.data.path);
if (context.mounted) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: const Text('Saved to gallery!'),
actions: [
FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
);
},
);
}
}
},
child: const Text(kIsWeb ? 'Download' : 'Save to gallery'),
)
],
),
),
);
}
Stack buildStack(BuildContext context, Widget? e, Uint8List data) {
return Stack(
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.45,
child: e,
),
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FloatingActionButton(
child: const Icon(
Icons.compress_rounded,
),
onPressed: () {
showDialog(
context: context,
builder: (context) => CompressionDialog(data: data),
);
},
),
),
)
],
);
}
Stack buildStackVid(BuildContext context, Widget? e, XFile data) {
return Stack(
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.45,
child: e,
),
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FloatingActionButton(
child: const Icon(
Icons.compress_rounded,
),
onPressed: () {
showDialog(
context: context,
builder: (context) => CompressionDialogVideo(data: data),
);
},
),
),
)
],
);
}
}
class CompressionDialog extends StatefulWidget {
const CompressionDialog({super.key, required this.data});
final Uint8List data;
@override
CompressionDialogState createState() => CompressionDialogState();
}
class CompressionDialogState extends State<CompressionDialog> {
int minWidth = 1920;
int minHeight = 1080;
int quality = 95;
int rotate = 0;
int inSampleSize = 1;
bool autoCorrectionAngle = true;
bool keepExif = false;
CompressFormat format = CompressFormat.jpeg;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Set Parameters'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSlider(
label: 'Min Width',
value: minWidth,
min: 0,
max: 5000,
divisions: 100,
onChanged: (value) {
setState(() {
minWidth = value.round();
});
},
),
_buildSlider(
label: 'Min Height',
value: minHeight,
min: 0,
max: 5000,
divisions: 100,
onChanged: (value) {
setState(() {
minHeight = value.round();
});
},
),
_buildSlider(
label: 'Quality',
value: quality,
min: 0,
max: 100,
divisions: 100,
onChanged: (value) {
setState(() {
quality = value.round();
});
},
),
_buildSlider(
label: 'Rotate',
value: rotate,
min: -180,
max: 180,
divisions: 360,
onChanged: (value) {
setState(() {
rotate = value.round();
});
},
),
_buildSlider(
label: 'In Sample Size',
value: inSampleSize,
min: 1,
max: 10,
divisions: 9,
onChanged: (value) {
setState(() {
inSampleSize = value.round();
});
},
),
const Text('Format:'),
DropdownButton<CompressFormat>(
value: format,
onChanged: (CompressFormat? newValue) {
setState(() {
format = newValue!;
});
},
items: CompressFormat.values.map((CompressFormat value) {
return DropdownMenuItem<CompressFormat>(
value: value,
child: Text(value.toString().split('.').last),
);
}).toList(),
),
Row(
children: [
const Text('Auto Correction Angle:'),
Checkbox(
value: autoCorrectionAngle,
onChanged: (value) {
setState(() {
autoCorrectionAngle = value!;
});
},
),
],
),
Row(
children: [
const Text('Keep Exif:'),
Checkbox(
value: keepExif,
onChanged: (value) {
setState(() {
keepExif = value!;
});
},
),
],
),
],
),
),
actions: [
TextButton(
onPressed: () async {
try {
await widget.data
.compressed(
autoCorrectionAngle: autoCorrectionAngle,
format: format,
inSampleSize: inSampleSize,
keepExif: keepExif,
minHeight: minHeight,
minWidth: minWidth,
quality: quality,
rotate: rotate,
)
.then(
(value) => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ImageComparisonScreen(
originalImage: widget.data,
compressedImage: value,
minWidth: minWidth,
minHeight: minHeight,
quality: quality,
rotate: rotate,
inSampleSize: inSampleSize,
autoCorrectionAngle: autoCorrectionAngle,
format: format,
keepExif: keepExif,
),
),
),
);
} catch (e) {
print(e);
if (context.mounted) {
showDialog(
context: context,
builder: (context) => ErrorDialog(
errorMessage: e.toString(),
),
);
}
}
if (context.mounted) {
Navigator.of(context).pop();
}
},
child: const Text('Save'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
],
);
}
Widget _buildSlider({
required String label,
required int value,
int? min,
required int max,
required int divisions,
required ValueChanged<double> onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label $value'),
Slider(
value: value.toDouble(),
min: min?.toDouble() ?? 0,
max: max.toDouble(),
divisions: divisions,
label: value.round().toString(),
onChanged: onChanged,
),
],
);
}
}
class ImageComparisonScreen extends StatelessWidget {
final Uint8List originalImage;
final Uint8List compressedImage;
final int minWidth;
final int minHeight;
final int quality;
final int rotate;
final int inSampleSize;
final bool autoCorrectionAngle;
final CompressFormat format;
final bool keepExif;
const ImageComparisonScreen({
super.key,
required this.originalImage,
required this.compressedImage,
required this.minWidth,
required this.minHeight,
required this.quality,
required this.rotate,
required this.inSampleSize,
required this.autoCorrectionAngle,
required this.format,
required this.keepExif,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Compression Overview'),
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Column(
children: [
Image.memory(
originalImage,
fit: BoxFit.contain,
),
const SizedBox(height: 10),
Text('Size: ${originalImage.lengthInBytes} bytes'),
],
),
),
const SizedBox(width: 10.0),
Expanded(
child: Column(
children: [
Image.memory(
compressedImage,
fit: BoxFit.contain,
),
const SizedBox(height: 10),
Text('Size: ${compressedImage.lengthInBytes} bytes'),
const SizedBox(height: 10),
_buildCompressionInfo(),
],
),
),
],
),
),
);
}
Widget _buildCompressionInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Compression Parameters:',
style: TextStyle(fontWeight: FontWeight.bold)),
Text('Min Width: $minWidth'),
Text('Min Height: $minHeight'),
Text('Quality: $quality'),
Text('Rotate: $rotate'),
Text('In Sample Size: $inSampleSize'),
Text('Auto Correction Angle: $autoCorrectionAngle'),
Text('Format: $format'),
Text('Keep Exif: $keepExif'),
],
);
}
}
class ErrorDialog extends StatelessWidget {
final String errorMessage;
const ErrorDialog({super.key, required this.errorMessage});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: SingleChildScrollView(
child: Text(errorMessage),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
);
}
}
class CompressionDialogVideo extends StatefulWidget {
const CompressionDialogVideo({Key? key, required this.data});
final XFile data;
@override
CompressionDialogVideoState createState() => CompressionDialogVideoState();
}
class CompressionDialogVideoState extends State<CompressionDialogVideo> {
VideoQuality quality = VideoQuality.DefaultQuality;
bool deleteOrigin = false;
int? startTime;
int? duration;
bool? includeAudio;
int frameRate = 30;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Set Parameters'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildQualityDropdown(),
Row(
children: [
const Text('Delete Origin:'),
Checkbox(
value: deleteOrigin,
onChanged: (value) {
setState(() {
deleteOrigin = value!;
});
},
),
],
),
_buildSlider(
label: 'Start Time',
value: startTime ?? 0,
min: 0,
max: 100,
divisions: 100,
onChanged: (value) {
setState(() {
startTime = value.round();
});
},
),
_buildSlider(
label: 'Duration',
value: duration ?? 0,
min: 0,
max: 100,
divisions: 100,
onChanged: (value) {
setState(() {
duration = value.round();
});
},
),
Row(
children: [
const Text('Include Audio:'),
Checkbox(
value: includeAudio ?? false,
onChanged: (value) {
setState(() {
includeAudio = value!;
});
},
),
],
),
_buildSlider(
label: 'Frame Rate',
value: frameRate,
min: 1,
max: 120,
divisions: 119,
onChanged: (value) {
setState(() {
frameRate = value.round();
});
},
),
],
),
),
actions: [
TextButton(
onPressed: () async {
try {
await VideoCompress.compressVideo(
widget.data.path,
startTime: startTime,
includeAudio: includeAudio,
frameRate: frameRate,
duration: duration,
deleteOrigin: deleteOrigin,
).then(
(value) => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VideoComparisonScreen(
originalVideo: widget.data,
compressedVideo: value!,
quality: quality,
deleteOrigin: deleteOrigin,
startTime: startTime,
duration: duration,
includeAudio: includeAudio,
frameRate: frameRate,
),
),
),
);
} catch (e) {
print(e);
if (context.mounted) {
showDialog(
context: context,
builder: (context) => ErrorDialog(
errorMessage: e.toString(),
),
);
}
}
if (context.mounted) {
Navigator.of(context).pop();
}
},
child: const Text('Save'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
],
);
}
Widget _buildSlider({
required String label,
required int value,
int? min,
required int max,
required int divisions,
required ValueChanged<double> onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label $value'),
Slider(
value: value.toDouble(),
min: min?.toDouble() ?? 0,
max: max.toDouble(),
divisions: divisions,
label: value.round().toString(),
onChanged: onChanged,
),
],
);
}
Widget _buildQualityDropdown() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Quality:'),
DropdownButton<VideoQuality>(
value: quality,
onChanged: (VideoQuality? newValue) {
setState(() {
quality = newValue!;
});
},
items: VideoQuality.values.map((VideoQuality value) {
return DropdownMenuItem<VideoQuality>(
value: value,
child: Text(value.toString().split('.').last),
);
}).toList(),
),
],
);
}
}
class VideoComparisonScreen extends StatelessWidget {
final XFile originalVideo;
final MediaInfo compressedVideo;
final VideoQuality quality;
final bool deleteOrigin;
final int? startTime;
final int? duration;
final bool? includeAudio;
final int frameRate;
const VideoComparisonScreen({
Key? key,
required this.originalVideo,
required this.compressedVideo,
required this.quality,
required this.deleteOrigin,
required this.startTime,
required this.duration,
required this.includeAudio,
required this.frameRate,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Compression Overview'),
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Column(
children: [
VideoPreviewWidget(path: originalVideo.path, autoplay: false),
const SizedBox(height: 10),
const Text(
'Original Video',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
// Display original video
],
),
),
const SizedBox(width: 10.0),
Expanded(
child: Column(
children: [
VideoPreviewWidget(path: compressedVideo.file!.path),
const Text(
'Compressed Video',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Text('Quality: $quality'),
Text('Delete Origin: $deleteOrigin'),
Text('Start Time: ${startTime ?? "Not Set"}'),
Text('Duration: ${duration ?? "Not Set"}'),
Text('Include Audio: ${includeAudio ?? "Not Set"}'),
Text('Frame Rate: $frameRate'),
const SizedBox(height: 10),
// Display compressed video
],
),
),
],
),
),
);
}
}