diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 0000000..4295bea
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,56 @@
+# Uncomment this line to define a global platform for your project
+platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+
+ # Ensure minimum iOS deployment target
+ target.build_configurations.each do |config|
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
+ config.build_settings['ENABLE_BITCODE'] = 'NO'
+
+ # Fix for Swift compatibility
+ if config.build_settings['WRAPPER_EXTENSION'] == 'bundle'
+ config.build_settings['DEVELOPMENT_TEAM'] = 'YourTeamID'
+ end
+ end
+ end
+end
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 06c9da8..4dd2459 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -45,5 +45,27 @@
UIApplicationSupportsIndirectInputEvents
+ NSCameraUsageDescription
+ This app needs camera access to capture and analyze shooting targets.
+ NSPhotoLibraryUsageDescription
+ This app needs photo library access to save and load target images.
+ NSPhotoLibraryAddUsageDescription
+ This app needs permission to save captured target images to your photo library.
+ NSMicrophoneUsageDescription
+ This app needs microphone access for video recording.
+ NSLocalNetworkUsageDescription
+ This app needs local network access to connect to camera streams.
+ NSBonjourServices
+
+ _http._tcp
+ _https._tcp
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+ NSAllowsLocalNetworking
+
+
diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart
index 1e063ee..2ddfd04 100644
--- a/lib/screens/calendar_screen.dart
+++ b/lib/screens/calendar_screen.dart
@@ -318,24 +318,6 @@ class _CalendarScreenState extends State {
color: isToday ? accentColor : Colors.black87,
),
),
- if (isToday) ...[
- const SizedBox(width: 8),
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- decoration: BoxDecoration(
- color: accentColor,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Text(
- _t('NOW'),
- style: const TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.bold,
- color: Colors.white,
- ),
- ),
- ),
- ],
],
),
),
diff --git a/lib/screens/camera_screen.dart b/lib/screens/camera_screen.dart
index a10cea3..04e9162 100644
--- a/lib/screens/camera_screen.dart
+++ b/lib/screens/camera_screen.dart
@@ -24,6 +24,10 @@ class _CameraScreenState extends State {
int _numberOfBullets = 5;
String _selectedLanguage = 'English';
+ // Brightness and contrast controls
+ double _brightness = 0.0; // -1.0 to 1.0
+ double _contrast = 1.0; // 0.0 to 2.0
+
@override
void initState() {
super.initState();
@@ -31,9 +35,24 @@ class _CameraScreenState extends State {
_initCamera();
_loadBulletColor(); // 👈
_loadLanguage();
+ _loadBrightnessContrast();
YOLODetector.create().then((detector) => _yoloDetector = detector);
}
+ Future _loadBrightnessContrast() async {
+ final prefs = await SharedPreferences.getInstance();
+ setState(() {
+ _brightness = prefs.getDouble('brightness') ?? 0.0;
+ _contrast = prefs.getDouble('contrast') ?? 1.0;
+ });
+ }
+
+ Future _saveBrightnessContrast() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setDouble('brightness', _brightness);
+ await prefs.setDouble('contrast', _contrast);
+ }
+
String _t(String key) {
return AppTranslations.translate(key, _selectedLanguage);
}
@@ -240,6 +259,175 @@ Color _getColorFromName(String colorName) {
}
+ void _showHelpDialog() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return Dialog(
+ backgroundColor: Colors.grey[900],
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _t('Help'),
+ style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 20),
+ _buildHelpItem(Icons.camera_alt, _t('Tap camera button to capture and detect bullets')),
+ const SizedBox(height: 12),
+ _buildHelpItem(Icons.filter_9_plus, _t('Bottom left number: Set expected bullet count (1-5)')),
+ const SizedBox(height: 12),
+ _buildHelpItem(Icons.brightness_6, _t('Sun icon: Adjust brightness and contrast for better visibility')),
+ const SizedBox(height: 12),
+ _buildHelpItem(Icons.crop_square, _t('Align target inside the square frame')),
+ const SizedBox(height: 20),
+ Center(
+ child: ElevatedButton(
+ onPressed: () => Navigator.of(context).pop(),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _bulletColor,
+ foregroundColor: Colors.white,
+ ),
+ child: Text(_t('Got it!')),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildHelpItem(IconData icon, String text) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, color: _bulletColor, size: 20),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ text,
+ style: const TextStyle(color: Colors.white, fontSize: 14),
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _showBrightnessContrastDialog() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return StatefulBuilder(
+ builder: (context, setDialogState) {
+ return Dialog(
+ backgroundColor: Colors.grey[900],
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ _t('Brightness & Contrast'),
+ style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 24),
+ // Brightness slider
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '${_t('Brightness')}: ${(_brightness * 100).toInt()}',
+ style: const TextStyle(color: Colors.white, fontSize: 16),
+ ),
+ Slider(
+ value: _brightness,
+ min: -1.0,
+ max: 1.0,
+ divisions: 40,
+ activeColor: _bulletColor,
+ onChanged: (value) {
+ setState(() {
+ _brightness = value;
+ });
+ setDialogState(() {});
+ _saveBrightnessContrast();
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ // Contrast slider
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '${_t('Contrast')}: ${(_contrast * 100).toInt()}%',
+ style: const TextStyle(color: Colors.white, fontSize: 16),
+ ),
+ Slider(
+ value: _contrast,
+ min: 0.5,
+ max: 2.0,
+ divisions: 30,
+ activeColor: _bulletColor,
+ onChanged: (value) {
+ setState(() {
+ _contrast = value;
+ });
+ setDialogState(() {});
+ _saveBrightnessContrast();
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ setState(() {
+ _brightness = 0.0;
+ _contrast = 1.0;
+ });
+ setDialogState(() {});
+ _saveBrightnessContrast();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.grey[800],
+ foregroundColor: Colors.white,
+ ),
+ child: Text(_t('Reset')),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _bulletColor,
+ foregroundColor: Colors.white,
+ ),
+ child: Text(_t('Done')),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
// Update the bullet count and save it
void _selectBulletCount(int number) {
setState(() {
@@ -274,26 +462,58 @@ Color _getColorFromName(String colorName) {
),
centerTitle: true,
actions: [
- // Display the selected bullet count
IconButton(
- icon: Text(
- '$_numberOfBullets',
- style: const TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.bold,
- color: Colors.white,
- ),
- ),
- onPressed: _showBulletDialog, // Show dialog on tap
+ icon: const Icon(Icons.help_outline, color: Colors.white),
+ onPressed: _showHelpDialog,
),
- const SizedBox(width: 16), // Space after the number
+ const SizedBox(width: 16),
],
),
bottomNavigationBar: BottomAppBar(
color: Colors.grey[900],
shape: const CircularNotchedRectangle(),
notchMargin: 8.0,
- child: SizedBox(height: 60),
+ child: SizedBox(
+ height: 60,
+ child: Row(
+ children: [
+ // Left - Bullet count (just number)
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: const EdgeInsets.only(left: 16.0),
+ child: InkWell(
+ onTap: _showBulletDialog,
+ child: Text(
+ '$_numberOfBullets',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ // Center space for FAB
+ const SizedBox(width: 80),
+ // Right section - Brightness
+ Expanded(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.brightness_6, color: Colors.white, size: 24),
+ onPressed: _showBrightnessContrastDialog,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
@@ -327,7 +547,7 @@ Color _getColorFromName(String colorName) {
height: screenSize.width * 0.9,
decoration: BoxDecoration(
border: Border.all(
- color: _bulletColor.withOpacity(0.9),
+ color: _bulletColor.withValues(alpha: 0.9),
width: 3,
),
),
diff --git a/lib/screens/stream_screen.dart b/lib/screens/stream_screen.dart
index 964d40d..36a7316 100644
--- a/lib/screens/stream_screen.dart
+++ b/lib/screens/stream_screen.dart
@@ -34,6 +34,10 @@ class _CameraStreamScreenState extends State with SingleTick
late YOLODetector _yoloDetector;
int _numberOfBullets = 5;
+ // Brightness and contrast controls
+ double _brightness = 0.0; // -1.0 to 1.0
+ double _contrast = 1.0; // 0.0 to 2.0
+
late AnimationController _zoomAnimController;
Animation? _zoomAnimation;
Matrix4? _originalMatrix;
@@ -53,6 +57,7 @@ class _CameraStreamScreenState extends State with SingleTick
_loadBulletCount();
_loadSelectedFilter();
_loadBulletColor(); // 👈 add this to actually load the saved bullet color
+ _loadBrightnessContrast();
YOLODetector.create().then((detector) => _yoloDetector = detector);
@@ -68,6 +73,20 @@ class _CameraStreamScreenState extends State with SingleTick
);
}
+ Future _loadBrightnessContrast() async {
+ final prefs = await SharedPreferences.getInstance();
+ setState(() {
+ _brightness = prefs.getDouble('brightness') ?? 0.0;
+ _contrast = prefs.getDouble('contrast') ?? 1.0;
+ });
+ }
+
+ Future _saveBrightnessContrast() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setDouble('brightness', _brightness);
+ await prefs.setDouble('contrast', _contrast);
+ }
+
Future _loadLanguage() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
@@ -428,6 +447,177 @@ Color _getColorFromName(String colorName) {
await prefs.setInt('bulletCount', number);
}
+ void _showHelpDialog() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return Dialog(
+ backgroundColor: Colors.grey[900],
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _t('Help'),
+ style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 20),
+ _buildHelpItem(Icons.camera_alt, _t('Tap camera button to capture and detect bullets')),
+ const SizedBox(height: 12),
+ _buildHelpItem(Icons.filter_9_plus, _t('Bottom left number: Set expected bullet count (1-5)')),
+ const SizedBox(height: 12),
+ _buildHelpItem(Icons.brightness_6, _t('Sun icon: Adjust brightness and contrast for better visibility')),
+ const SizedBox(height: 12),
+ _buildHelpItem(Icons.filter_alt, _t('Filter icon: Apply color filters (None, Greyscale, Invert)')),
+ const SizedBox(height: 12),
+ _buildHelpItem(Icons.pinch, _t('Pinch to zoom on the camera stream')),
+ const SizedBox(height: 20),
+ Center(
+ child: ElevatedButton(
+ onPressed: () => Navigator.of(context).pop(),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _bulletColor,
+ foregroundColor: Colors.white,
+ ),
+ child: Text(_t('Got it!')),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildHelpItem(IconData icon, String text) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, color: _bulletColor, size: 20),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ text,
+ style: const TextStyle(color: Colors.white, fontSize: 14),
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _showBrightnessContrastDialog() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return StatefulBuilder(
+ builder: (context, setDialogState) {
+ return Dialog(
+ backgroundColor: Colors.grey[900],
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ _t('Brightness & Contrast'),
+ style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 24),
+ // Brightness slider
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '${_t('Brightness')}: ${(_brightness * 100).toInt()}',
+ style: const TextStyle(color: Colors.white, fontSize: 16),
+ ),
+ Slider(
+ value: _brightness,
+ min: -1.0,
+ max: 1.0,
+ divisions: 40,
+ activeColor: _bulletColor,
+ onChanged: (value) {
+ setState(() {
+ _brightness = value;
+ });
+ setDialogState(() {});
+ _saveBrightnessContrast();
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ // Contrast slider
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '${_t('Contrast')}: ${(_contrast * 100).toInt()}%',
+ style: const TextStyle(color: Colors.white, fontSize: 16),
+ ),
+ Slider(
+ value: _contrast,
+ min: 0.5,
+ max: 2.0,
+ divisions: 30,
+ activeColor: _bulletColor,
+ onChanged: (value) {
+ setState(() {
+ _contrast = value;
+ });
+ setDialogState(() {});
+ _saveBrightnessContrast();
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ setState(() {
+ _brightness = 0.0;
+ _contrast = 1.0;
+ });
+ setDialogState(() {});
+ _saveBrightnessContrast();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.grey[800],
+ foregroundColor: Colors.white,
+ ),
+ child: Text(_t('Reset')),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _bulletColor,
+ foregroundColor: Colors.white,
+ ),
+ child: Text(_t('Done')),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
@override
void dispose() {
_zoomAnimController.dispose();
@@ -449,24 +639,70 @@ void dispose() {
}
ColorFilter _buildColorFilter() {
+ // Base filter matrix
+ List baseMatrix;
+
switch (_selectedFilter) {
case ImageFilterType.greyscale:
- return const ColorFilter.matrix([
+ baseMatrix = [
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0, 0, 0, 1, 0,
- ]);
+ ];
+ break;
case ImageFilterType.invert:
- return const ColorFilter.matrix([
- -1, 0, 0, 0, 255,
- 0, -1, 0, 0, 255,
- 0, 0, -1, 0, 255,
- 0, 0, 0, 1, 0,
- ]);
+ baseMatrix = [
+ -1.0, 0.0, 0.0, 0.0, 255.0,
+ 0.0, -1.0, 0.0, 0.0, 255.0,
+ 0.0, 0.0, -1.0, 0.0, 255.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0,
+ ];
+ break;
default:
- return const ColorFilter.mode(Colors.transparent, BlendMode.dst);
+ baseMatrix = [
+ 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0,
+ ];
}
+
+ // Apply brightness and contrast adjustments
+ final b = _brightness * 255.0; // Convert to 0-255 range
+ final c = _contrast;
+
+ // Combine filter with brightness/contrast
+ final adjustedMatrix = [
+ c, 0.0, 0.0, 0.0, b,
+ 0.0, c, 0.0, 0.0, b,
+ 0.0, 0.0, c, 0.0, b,
+ 0.0, 0.0, 0.0, 1.0, 0.0,
+ ];
+
+ // Multiply base matrix with adjustment matrix
+ return ColorFilter.matrix(_multiplyMatrices(baseMatrix, adjustedMatrix));
+}
+
+List _multiplyMatrices(List a, List b) {
+ // For simplicity, just apply brightness/contrast on top of base filter
+ final result = [];
+
+ for (int i = 0; i < a.length; i++) {
+ result.add(a[i]);
+ }
+
+ // Apply contrast to RGB components
+ result[0] = result[0] * b[0]; // R
+ result[6] = result[6] * b[6]; // G
+ result[12] = result[12] * b[12]; // B
+
+ // Apply brightness
+ result[4] = result[4] + b[4]; // R offset
+ result[9] = result[9] + b[9]; // G offset
+ result[14] = result[14] + b[14]; // B offset
+
+ return result;
}
@override
@@ -482,19 +718,8 @@ void dispose() {
),
actions: [
IconButton(
- icon: Text(
- '$_numberOfBullets',
- style: const TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.bold,
- color: Colors.white,
- ),
- ),
- onPressed: _showBulletDialog,
- ),
- IconButton(
- icon: const Icon(Icons.filter_alt, color: Colors.white),
- onPressed: _showFilterDialog,
+ icon: const Icon(Icons.help_outline, color: Colors.white),
+ onPressed: _showHelpDialog,
),
const SizedBox(width: 16),
],
@@ -507,31 +732,33 @@ void dispose() {
child: LayoutBuilder(
builder: (context, constraints) {
return Center(
- child: InteractiveViewer(
- clipBehavior: Clip.hardEdge,
- constrained: false,
- boundaryMargin: EdgeInsets.zero,
- minScale: 1.0,
- maxScale: 5.0,
- transformationController: _transformationController,
- child: SizedBox(
- width: constraints.maxWidth,
- height: constraints.maxHeight,
- child: (_isError || _streamUrl == null)
- ? _buildErrorContent()
- : RepaintBoundary(
- child: ColorFiltered(
- colorFilter: _buildColorFilter(),
- child: Mjpeg(
- stream: _streamUrl!,
- isLive: true,
- fit: BoxFit.contain,
- error: (context, error, stackTrace) => _buildErrorContent(),
+ child: (_isError || _streamUrl == null)
+ ? _buildErrorContent()
+ : InteractiveViewer(
+ clipBehavior: Clip.hardEdge,
+ constrained: false,
+ boundaryMargin: EdgeInsets.zero,
+ minScale: 1.0,
+ maxScale: 5.0,
+ transformationController: _transformationController,
+ child: SizedBox(
+ width: constraints.maxWidth,
+ height: constraints.maxHeight,
+ child: RepaintBoundary(
+ child: ColorFiltered(
+ colorFilter: _buildColorFilter(),
+ child: Mjpeg(
+ stream: _streamUrl!,
+ isLive: true,
+ fit: BoxFit.contain,
+ error: (context, error, stackTrace) {
+ return _buildErrorContent();
+ },
+ ),
+ ),
),
),
),
- ),
- ),
);
},
),
@@ -540,7 +767,7 @@ void dispose() {
if (_isCapturing)
Positioned.fill(
child: Container(
- color: Colors.black.withOpacity(0.4),
+ color: Colors.black.withValues(alpha: 0.4),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -570,11 +797,45 @@ void dispose() {
notchMargin: 8.0,
child: SizedBox(
height: 60,
- child: Stack(
- alignment: Alignment.center,
+ child: Row(
children: [
- Align(
- alignment: Alignment.center,
+ // Left - Bullet count (just number)
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: const EdgeInsets.only(left: 16.0),
+ child: InkWell(
+ onTap: _showBulletDialog,
+ child: Text(
+ '$_numberOfBullets',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ // Center space for FAB
+ const SizedBox(width: 80),
+ // Right section - Brightness and Filter
+ Expanded(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.brightness_6, color: Colors.white, size: 24),
+ onPressed: _showBrightnessContrastDialog,
+ ),
+ IconButton(
+ icon: const Icon(Icons.filter_alt, color: Colors.white, size: 24),
+ onPressed: _showFilterDialog,
+ ),
+ ],
+ ),
),
],
),
diff --git a/lib/screens/target_screen.dart b/lib/screens/target_screen.dart
index 15e0b11..1e2452d 100644
--- a/lib/screens/target_screen.dart
+++ b/lib/screens/target_screen.dart
@@ -63,6 +63,7 @@ class _TargetScreenState extends State {
});
}
+
@override
Widget build(BuildContext context) {
return Scaffold(
diff --git a/lib/utils/translations.dart b/lib/utils/translations.dart
index 9124f0f..f0aeb0e 100644
--- a/lib/utils/translations.dart
+++ b/lib/utils/translations.dart
@@ -342,6 +342,94 @@ class AppTranslations {
'Slovenian': 'Ni mogoče naložiti kamere',
},
+ // Mode Selection
+ 'Select Detection Mode': {
+ 'English': 'Select Detection Mode',
+ 'Slovenian': 'Izberi način zaznavanja',
+ },
+ 'Capture & Shoot': {
+ 'English': 'Capture & Shoot',
+ 'Slovenian': 'Zajemi in streljaj',
+ },
+ 'Take single photo and detect bullets': {
+ 'English': 'Take single photo and detect bullets',
+ 'Slovenian': 'Zajemi fotografijo in zazna zadetke',
+ },
+ 'Live Detection': {
+ 'English': 'Live Detection',
+ 'Slovenian': 'Živa zaznava',
+ },
+ 'Continuous monitoring with real-time scoring': {
+ 'English': 'Continuous monitoring with real-time scoring',
+ 'Slovenian': 'Neprekinjeno spremljanje z ocenjevanjem v živo',
+ },
+
+ // Live Detection Screen
+ 'Capture Baseline': {
+ 'English': 'Capture Baseline',
+ 'Slovenian': 'Zajemi osnovno sliko',
+ },
+ 'Position target and capture empty baseline': {
+ 'English': 'Position target and capture empty baseline',
+ 'Slovenian': 'Postavi tarčo in zajemi prazno osnovno sliko',
+ },
+ 'Start Detection': {
+ 'English': 'Start Detection',
+ 'Slovenian': 'Začni zaznavanje',
+ },
+ 'Stop Detection': {
+ 'English': 'Stop Detection',
+ 'Slovenian': 'Ustavi zaznavanje',
+ },
+ 'Bullets Detected': {
+ 'English': 'Bullets Detected',
+ 'Slovenian': 'Zaznani zadetki',
+ },
+ 'Current Score': {
+ 'English': 'Current Score',
+ 'Slovenian': 'Trenutna ocena',
+ },
+ 'Monitoring...': {
+ 'English': 'Monitoring...',
+ 'Slovenian': 'Spremljanje...',
+ },
+ 'Save & Exit': {
+ 'English': 'Save & Exit',
+ 'Slovenian': 'Shrani in zapri',
+ },
+ 'Capturing baseline...': {
+ 'English': 'Capturing baseline...',
+ 'Slovenian': 'Zajemanje osnove...',
+ },
+ 'Baseline captured!': {
+ 'English': 'Baseline captured!',
+ 'Slovenian': 'Osnova zajeta!',
+ },
+ 'Analyzing frame...': {
+ 'English': 'Analyzing frame...',
+ 'Slovenian': 'Analiza slike...',
+ },
+ 'New bullet detected!': {
+ 'English': 'New bullet detected!',
+ 'Slovenian': 'Nov zadetek zaznan!',
+ },
+ 'Calculate Score': {
+ 'English': 'Calculate Score',
+ 'Slovenian': 'Izračunaj rezultat',
+ },
+ 'Target reached': {
+ 'English': 'Target reached',
+ 'Slovenian': 'Cilj dosežen',
+ },
+ 'bullets': {
+ 'English': 'bullets',
+ 'Slovenian': 'zadetkov',
+ },
+ 'Click Calculate Score to continue': {
+ 'English': 'Click Calculate Score to continue',
+ 'Slovenian': 'Kliknite Izračunaj rezultat za nadaljevanje',
+ },
+
// Delete confirmations
'Are you sure you want to delete this file?': {
'English': 'Are you sure you want to delete this file?',
@@ -367,6 +455,62 @@ class AppTranslations {
'English': 'and all of its contents?',
'Slovenian': 'in vso njeno vsebino?',
},
+
+ // Help dialog translations
+ 'Help': {
+ 'English': 'Help',
+ 'Slovenian': 'Pomoč',
+ },
+ 'Tap camera button to capture and detect bullets': {
+ 'English': 'Tap camera button to capture and detect bullets',
+ 'Slovenian': 'Tapnite gumb kamere za zajem in zaznavanje zadetkov',
+ },
+ 'Bottom left number: Set expected bullet count (1-5)': {
+ 'English': 'Bottom left number: Set expected bullet count (1-5)',
+ 'Slovenian': 'Spodaj levo številka: Nastavite število zadetkov (1-5)',
+ },
+ 'Sun icon: Adjust brightness and contrast for better visibility': {
+ 'English': 'Sun icon: Adjust brightness and contrast for better visibility',
+ 'Slovenian': 'Ikona sonca: Prilagodite svetlost in kontrast za boljšo vidljivost',
+ },
+ 'Filter icon: Apply color filters (None, Greyscale, Invert)': {
+ 'English': 'Filter icon: Apply color filters (None, Greyscale, Invert)',
+ 'Slovenian': 'Ikona filtra: Uporabite barvne filtre (Brez, Sivine, Inverzno)',
+ },
+ 'Pinch to zoom on the camera stream': {
+ 'English': 'Pinch to zoom on the camera stream',
+ 'Slovenian': 'Povečajte s prstoma na video pretoku',
+ },
+ 'Align target inside the square frame': {
+ 'English': 'Align target inside the square frame',
+ 'Slovenian': 'Poravnajte tarčo znotraj kvadratnega okvirja',
+ },
+ 'Got it!': {
+ 'English': 'Got it!',
+ 'Slovenian': 'Razumem!',
+ },
+
+ // Brightness & Contrast dialog translations
+ 'Brightness & Contrast': {
+ 'English': 'Brightness & Contrast',
+ 'Slovenian': 'Svetlost in kontrast',
+ },
+ 'Brightness': {
+ 'English': 'Brightness',
+ 'Slovenian': 'Svetlost',
+ },
+ 'Contrast': {
+ 'English': 'Contrast',
+ 'Slovenian': 'Kontrast',
+ },
+ 'Reset': {
+ 'English': 'Reset',
+ 'Slovenian': 'Ponastavi',
+ },
+ 'Done': {
+ 'English': 'Done',
+ 'Slovenian': 'Končano',
+ },
};
static String translate(String key, String language) {