From 2e3a69a23ed12c35850293921d94aca2305564dd Mon Sep 17 00:00:00 2001 From: bl3kunja Date: Thu, 16 Oct 2025 17:42:46 +0200 Subject: [PATCH] V1.0.0 --- .vscode/settings.json | 4 + lib/screens/book_screen.dart | 445 ++++++++++++++++++++++--------- lib/screens/calendar_screen.dart | 414 ++++++++++------------------ lib/screens/camera_screen.dart | 31 ++- lib/screens/home_screen.dart | 42 ++- lib/screens/preview_screen.dart | 158 ++++++++--- lib/screens/settings_screen.dart | 200 ++++++++++++-- lib/screens/stream_screen.dart | 58 ++-- lib/screens/target_screen.dart | 188 +++++++------ lib/screens/target_view.dart | 87 ++++-- lib/utils/translations.dart | 379 ++++++++++++++++++++++++++ lib/widgets/bottom_nav.dart | 19 +- 12 files changed, 1421 insertions(+), 604 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/utils/translations.dart diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..219550d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "idf.pythonInstallPath": "/usr/bin/python3", + "cmake.ignoreCMakeListsMissing": true +} \ No newline at end of file diff --git a/lib/screens/book_screen.dart b/lib/screens/book_screen.dart index dce77b6..11a21fa 100644 --- a/lib/screens/book_screen.dart +++ b/lib/screens/book_screen.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.dart'; // For DateFormat import 'package:path_provider/path_provider.dart'; // For getApplicationDocumentsDirectory import 'target_view.dart'; // Import the TargetViewScreen import 'package:shared_preferences/shared_preferences.dart'; +import '../utils/translations.dart'; class BookScreen extends StatefulWidget { const BookScreen({super.key}); @@ -17,21 +18,34 @@ class _BookScreenState extends State with WidgetsBindingObserver { Map> _groupedFiles = {}; // Files grouped by folder Map _folderCreationDates = {}; // Folder creation dates Map _folderScores = {}; // Folder scores (sum of scores of all target files inside) - String? _expandedFolder; Color _bulletColor = Colors.blue; // default fallback Map _folderTypes = {}; // true = tournament, false = training String _folderFilter = 'all'; bool isRenameEnabled = false; String _sortMode = 'date'; // or 'score' + String _selectedLanguage = 'English'; + String? _expandedFolder; // Track which folder is expanded @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // Register to listen for lifecycle events _loadBulletColor(); + _loadLanguage(); _loadTargetDataFiles(); // Initial load when the screen is first created } + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + @override void dispose() { WidgetsBinding.instance.removeObserver(this); // Unregister from lifecycle events when disposed @@ -192,7 +206,7 @@ Color _getColorFromName(String colorName) { Future _deleteFile(File file) async { bool? confirmDeletion = await _showConfirmationDialog( context, - 'Are you sure you want to delete this file?', + _t('Are you sure you want to delete this file?'), ); if (confirmDeletion == true) { @@ -309,9 +323,9 @@ Future _renameFolder(String oldFolderName) async { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( - 'Folder Settings', - style: TextStyle( + Text( + _t('Folder Settings'), + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -325,7 +339,7 @@ Future _renameFolder(String oldFolderName) async { decoration: InputDecoration( filled: true, fillColor: Colors.grey[800], - hintText: 'Enter new folder name', + hintText: _t('Enter new folder name'), hintStyle: const TextStyle(color: Colors.white54), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), enabledBorder: OutlineInputBorder( @@ -363,7 +377,7 @@ Future _renameFolder(String oldFolderName) async { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Center( child: Text( - isTournament ? 'Tournament Mode 🏆' : 'Mark as Tournament 🏆', + isTournament ? _t('Tournament Mode 🏆') : _t('Mark as Tournament 🏆'), style: TextStyle( color: isTournament ? _bulletColor : Colors.white70, fontWeight: FontWeight.w500, @@ -387,7 +401,7 @@ Future _renameFolder(String oldFolderName) async { borderRadius: BorderRadius.circular(12), ), ), - child: const Text('Cancel'), + child: Text(_t('Cancel')), ), ), const SizedBox(width: 12), @@ -405,7 +419,7 @@ Future _renameFolder(String oldFolderName) async { borderRadius: BorderRadius.circular(12), ), ), - child: const Text('Apply'), + child: Text(_t('Apply')), ), ), ], @@ -471,7 +485,7 @@ Future _renameFolder(String oldFolderName) async { Future _deleteFolder(String folderName) async { bool? confirmDeletion = await _showConfirmationDialog( context, - 'Are you sure you want to delete the folder "$folderName" and all of its contents?', + '${_t('Are you sure you want to delete the folder')} "$folderName" ${_t('and all of its contents?')}', ); if (confirmDeletion == true) { @@ -485,12 +499,6 @@ Future _renameFolder(String oldFolderName) async { _loadTargetDataFiles(); // Refresh file/folder list - if (_expandedFolder == folderName) { - setState(() { - _expandedFolder = null; - }); - } - ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Folder "$folderName" deleted successfully.'), @@ -517,9 +525,9 @@ Future _renameFolder(String oldFolderName) async { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( - 'Confirm Deletion', - style: TextStyle( + Text( + _t('Confirm Deletion'), + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -547,7 +555,7 @@ Future _renameFolder(String oldFolderName) async { borderRadius: BorderRadius.circular(12), ), ), - child: const Text("Yes, delete"), + child: Text(_t("Yes, delete")), ), ), const SizedBox(height: 12), @@ -565,7 +573,7 @@ Future _renameFolder(String oldFolderName) async { borderRadius: BorderRadius.circular(12), ), ), - child: const Text("Cancel"), + child: Text(_t("Cancel")), ), ), ], @@ -582,125 +590,292 @@ Widget _buildFolderRow(String folderName) { final totalScore = _folderScores[folderName]; final folderDate = _folderCreationDates[folderName]; final formattedDate = folderDate != null - ? DateFormat('yyyy-MM-dd').format(folderDate) + ? DateFormat('dd/MM/yyyy').format(folderDate) : 'Unknown Date'; - final isExpanded = _expandedFolder == folderName; - return Container( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), decoration: BoxDecoration( - color: isExpanded ? _bulletColor.withOpacity(0.15) : Colors.grey[300], + color: Colors.grey[300], borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isExpanded ? _bulletColor : Colors.transparent, - width: 1.5, - ), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListTile( - dense: true, // Compact layout - contentPadding: EdgeInsets.zero, - title: Text( - _folderTypes[folderName] == true - ? '🏆 $folderName' - : folderName, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), - subtitle: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - "📝 $numberOfTargets / 🎯 $totalScore / 📅 $formattedDate", - style: const TextStyle(fontSize: 13, color: Colors.black87), + // Header section: Name and Date + InkWell( + onTap: () { + setState(() { + _expandedFolder = _expandedFolder == folderName ? null : folderName; + }); + }, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + // Small colored indicator + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: _bulletColor, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + Text( + _folderTypes[folderName] == true + ? '🏆 $folderName' + : folderName, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ], + ), + Row( + children: [ + Text( + "📅 $formattedDate", + style: const TextStyle(fontSize: 13, color: Colors.black87), + ), + const SizedBox(width: 8), + Icon( + _expandedFolder == folderName ? Icons.expand_less : Icons.expand_more, + color: Colors.black87, + ), + ], + ), + ], ), ), - trailing: Row( - mainAxisSize: MainAxisSize.min, + ), + // Expandable targets list (in the middle) + if (_expandedFolder == folderName) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + ), + child: _buildTargetsList(folderName), + ), + ], + // Bottom section: Info badge and action buttons + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - IconButton( - iconSize: 22, - visualDensity: VisualDensity.compact, - icon: Icon(Icons.my_location, color: _bulletColor), - onPressed: () => _openFolderTargetView(folderName), + // Info badge section (non-clickable, matches button style) + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.only( + bottomLeft: const Radius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Targets count + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.filter_center_focus, size: 16, color: Colors.black87), + const SizedBox(width: 4), + Text( + "$numberOfTargets", + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87), + ), + ], + ), + // Score + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.emoji_events, size: 16, color: Colors.amber), + const SizedBox(width: 4), + Text( + "$totalScore", + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87), + ), + ], + ), + ], + ), + ), ), - IconButton( - iconSize: 22, - visualDensity: VisualDensity.compact, - icon: Icon(Icons.edit, color: _bulletColor), - onPressed: () => _renameFolder(folderName), + // Preview button section + InkWell( + onTap: () => _openFolderTargetView(folderName), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: _bulletColor.withValues(alpha: 0.2), + border: Border( + left: BorderSide(color: Colors.grey[400]!, width: 1), + ), + ), + child: Icon(Icons.visibility, color: _bulletColor, size: 20), + ), ), - IconButton( - iconSize: 22, - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () => _deleteFolder(folderName), + // Edit button section + InkWell( + onTap: () => _renameFolder(folderName), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: _bulletColor.withValues(alpha: 0.2), + border: Border( + left: BorderSide(color: Colors.grey[400]!, width: 1), + ), + ), + child: Icon(Icons.edit, color: _bulletColor, size: 20), + ), + ), + // Delete button section + InkWell( + onTap: () => _deleteFolder(folderName), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.2), + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(12), + ), + border: Border( + left: BorderSide(color: Colors.grey[400]!, width: 1), + ), + ), + child: const Icon(Icons.delete, color: Colors.red, size: 20), + ), ), ], ), - onTap: () { - setState(() { - _expandedFolder = isExpanded ? null : folderName; - }); - }, ), - if (isExpanded) - ..._groupedFiles[folderName]! - .asMap() - .entries - .map((entry) => _buildFileRow(entry.value, folderName, entry.key)) - .toList(), ], ), ); } - // Build the row for files in the folder with everything in a single row - Widget _buildFileRow(File file, String folderName, int index) { - final fileName = "Target ${index + 1}"; // Use number starting from 1 - final folderDate = _folderCreationDates[folderName]; - final formattedDate = folderDate != null - ? DateFormat('yyyy-MM-dd').format(folderDate) - : 'Unknown Date'; // or any default fallback string +Widget _buildTargetsList(String folderName) { + final targetFiles = _groupedFiles[folderName] ?? []; - final score = _getScoreFromJson(jsonDecode(file.readAsStringSync())); // Read the score for the file - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // Reduced vertical padding for compact height - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), // Tight margin - decoration: BoxDecoration( - color: Colors.grey[200], // Light grey background for files - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey[300]!, width: 1), // Light border around the file - ), - child: InkWell( - onTap: () => _openFilePreview(file), // Open the file preview when the row is tapped - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, // Distribute space across the row - children: [ - // File name - Text(fileName, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - - // Date - Text(formattedDate, style: const TextStyle(fontSize: 12, color: Colors.black54)), - - // Target emoji and score - Text("🎯 $score", style: const TextStyle(fontSize: 14, color: Colors.black54)), - - // Delete button - IconButton( - icon: const Icon(Icons.delete, color: Color.fromARGB(255, 252, 102, 92)), - onPressed: () => _deleteFile(file), // Trigger file deletion - ), - ], + if (targetFiles.isEmpty) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + _t("No targets in this folder"), + style: const TextStyle(color: Colors.black54, fontSize: 14), + textAlign: TextAlign.center, ), - ), + ); + } + + return Column( + children: targetFiles.asMap().entries.map((entry) { + final index = entry.key; + final file = entry.value; + final score = _getScoreFromJson(jsonDecode(file.readAsStringSync())); + final fileName = "${_t('Target')} ${index + 1}"; + + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey[300]!, width: 1), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Target info section + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + child: Row( + children: [ + Expanded( + child: Text( + fileName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.emoji_events, + color: Colors.amber, + size: 14, + ), + const SizedBox(width: 4), + Text( + "$score", + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ], + ), + ), + ), + // Preview button + InkWell( + onTap: () => _openFilePreview(file), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: _bulletColor.withValues(alpha: 0.15), + border: Border( + left: BorderSide(color: Colors.grey[300]!, width: 1), + ), + ), + child: Icon(Icons.visibility, color: _bulletColor, size: 18), + ), + ), + // Delete button + InkWell( + onTap: () => _deleteFile(file), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + border: Border( + left: BorderSide(color: Colors.grey[300]!, width: 1), + ), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + child: const Icon( + Icons.delete, + color: Colors.red, + size: 18, + ), + ), + ), + ], + ), + ), + ); + }).toList(), ); } + @override Widget build(BuildContext context) { return Scaffold( @@ -723,10 +898,10 @@ Widget build(BuildContext context) { children: [ Text( _folderFilter == 'all' - ? 'Showing: 📂 All folders' + ? _t('Showing: 📂 All folders') : _folderFilter == 'tournament' - ? 'Showing: 🏆 Tournament folders' - : 'Showing: 🎯 Training folders', + ? _t('Showing: 🏆 Tournament folders') + : _t('Showing: 🎯 Training folders'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const Icon(Icons.filter_list), @@ -738,7 +913,7 @@ Widget build(BuildContext context) { // 📁 Folder list or empty state Expanded( child: _groupedFiles.isEmpty - ? const Center(child: Text("No saved target data found", style: TextStyle(fontSize: 18))) + ? Center(child: Text(_t("No saved target data found"), style: const TextStyle(fontSize: 18))) : Scrollbar( thumbVisibility: true, // always show scrollbar thickness: 6, // optional: adjust width @@ -771,9 +946,9 @@ void _showFolderFilterPopup(BuildContext context) { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Filter Folders', - style: TextStyle( + Text( + _t('Filter Folders'), + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -782,21 +957,22 @@ void _showFolderFilterPopup(BuildContext context) { const SizedBox(height: 24), // Folder type filters - _buildStyledFilterButton('all', '📂 All folders', context), + _buildStyledFilterButton('all', _t('📂 All folders'), context), const SizedBox(height: 12), - _buildStyledFilterButton('tournament', '🏆 Tournament', context), + _buildStyledFilterButton('tournament', _t('🏆 Tournament'), context), const SizedBox(height: 12), - _buildStyledFilterButton('training', '🎯 Training', context), + _buildStyledFilterButton('training', _t('🎯 Training'), context), const SizedBox(height: 24), - const Text( - 'Sort By', - style: TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.w500), + Text( + _t('Sort By'), + style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 12), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: GestureDetector( @@ -805,20 +981,23 @@ void _showFolderFilterPopup(BuildContext context) { this.setState(() {}); // 👈 refresh parent widget }, child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + margin: const EdgeInsets.only(right: 6), decoration: BoxDecoration( color: _sortMode == 'date' ? _bulletColor : Colors.grey[800], borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( - '📅 Sort by Date', + _t('📅 Sort by Date'), + textAlign: TextAlign.center, style: TextStyle( color: _sortMode == 'date' ? Colors.white : Colors.white70, fontWeight: FontWeight.w500, - fontSize: 15, + fontSize: 14, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), ), @@ -831,19 +1010,23 @@ void _showFolderFilterPopup(BuildContext context) { this.setState(() {}); // 👈 refresh parent widget }, child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + margin: const EdgeInsets.only(left: 6), decoration: BoxDecoration( color: _sortMode == 'score' ? _bulletColor : Colors.grey[800], borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( - '🏆 Sort by Score', + _t('🏆 Sort by Score'), + textAlign: TextAlign.center, style: TextStyle( color: _sortMode == 'score' ? Colors.white : Colors.white70, fontWeight: FontWeight.w500, - fontSize: 15, + fontSize: 14, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), ), diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 78c4a47..1e063ee 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:intl/date_symbol_data_local.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/google_calendar_service.dart'; +import '../utils/translations.dart'; class CalendarScreen extends StatefulWidget { const CalendarScreen({super.key}); @@ -16,14 +18,28 @@ class _CalendarScreenState extends State { List _events = []; String? _errorMessage; String _bulletColor = 'Blue'; + String _selectedLanguage = 'English'; @override void initState() { super.initState(); + initializeDateFormatting('sl_SI', null); // Initialize Slovenian locale _loadBulletColor(); + _loadLanguage(); _loadEvents(); } + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + @override void dispose() { _calendarService.dispose(); @@ -128,15 +144,21 @@ class _CalendarScreenState extends State { return a.startTime!.compareTo(b.startTime!); }); - setState(() { - _events = filteredEvents; - }); + if (mounted) { + setState(() { + _events = filteredEvents; + }); + } } catch (e) { - setState(() { - _errorMessage = 'Error loading events: $e'; - }); + if (mounted) { + setState(() { + _errorMessage = 'Error loading events: $e'; + }); + } } finally { - setState(() => _isLoading = false); + if (mounted) { + setState(() => _isLoading = false); + } } } @@ -172,7 +194,7 @@ class _CalendarScreenState extends State { ElevatedButton.icon( onPressed: _loadEvents, icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: Text(_t('Retry')), style: ElevatedButton.styleFrom( backgroundColor: accentColor, foregroundColor: Colors.white, @@ -199,13 +221,13 @@ class _CalendarScreenState extends State { children: [ Icon(Icons.event_available, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), - const Text( - 'No upcoming events', - style: TextStyle(fontSize: 16, color: Colors.grey), + Text( + _t('No upcoming events'), + style: const TextStyle(fontSize: 16, color: Colors.grey), ), const SizedBox(height: 8), Text( - 'Pull down to refresh', + _t('Pull down to refresh'), style: TextStyle(fontSize: 14, color: Colors.grey[400]), ), ], @@ -229,15 +251,6 @@ class _CalendarScreenState extends State { final startTime = event.startTime; final isAllDay = event.isAllDay; - String timeText; - if (isAllDay) { - timeText = 'All day'; - } else if (startTime != null) { - timeText = DateFormat('HH:mm').format(startTime.toLocal()); - } else { - timeText = ''; - } - // Format the date for display String dateText = ''; bool isToday = false; @@ -248,300 +261,163 @@ class _CalendarScreenState extends State { final difference = eventDay.difference(today).inDays; if (difference == 0) { - dateText = 'Today'; + dateText = _t('Today'); isToday = true; } else if (difference == 1) { - dateText = 'Tomorrow'; + dateText = _t('Tomorrow'); } else { - dateText = DateFormat('EEEE, MMMM d').format(startTime); + // Use locale based on selected language + final locale = _selectedLanguage == 'Slovenian' ? 'sl_SI' : 'en_US'; + dateText = DateFormat('EEEE, MMMM d', locale).format(startTime); + // Capitalize first letter of day and month + if (dateText.isNotEmpty) { + dateText = dateText[0].toUpperCase() + dateText.substring(1); + } } } return Container( margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), decoration: BoxDecoration( - color: Colors.white, + color: Colors.grey[300], borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _showEventDetails(event, accentColor), - borderRadius: BorderRadius.circular(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Date header - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: isToday - ? accentColor.withValues(alpha: 0.15) - : Colors.grey[50], - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Date header + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isToday + ? accentColor.withValues(alpha: 0.15) + : Colors.grey[200], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + children: [ + // Small colored indicator + Container( + width: 4, + height: 16, + decoration: BoxDecoration( + color: accentColor, + borderRadius: BorderRadius.circular(2), ), ), - child: Row( - children: [ - Container( - width: 4, - height: 16, - decoration: BoxDecoration( - color: accentColor, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 8), - Text( - dateText, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - 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: const Text( - 'NOW', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ], - ], + const SizedBox(width: 8), + Text( + dateText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isToday ? accentColor : Colors.black87, + ), ), - ), - // Time and title - Padding( - padding: const EdgeInsets.all(16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Time indicator with accent color - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Center( - child: Text( - timeText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: isAllDay ? 12 : 14, - fontWeight: FontWeight.bold, - color: accentColor, - ), - ), + 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, ), ), - const SizedBox(width: 16), - // Title + ), + ], + ], + ), + ), + // Main content + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title with icon + Row( + children: [ + Icon(Icons.event, size: 20, color: accentColor), + const SizedBox(width: 8), Expanded( child: Text( - event.summary ?? 'Untitled Event', + event.summary ?? _t('Untitled Event'), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Colors.black87, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), ), - const SizedBox(width: 8), - // Arrow icon - Icon( - Icons.arrow_forward_ios, - size: 16, - color: Colors.grey[400], - ), ], ), - ), - ], - ), - ), - ), - ); - } - - void _showEventDetails(CalendarEvent event, Color accentColor) { - final startTime = event.startTime; - final endTime = event.endTime; - final isAllDay = event.isAllDay; - - showDialog( - context: context, - builder: (context) => Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Container( - constraints: const BoxConstraints(maxWidth: 400), - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 4, - height: 24, - decoration: BoxDecoration( - color: accentColor, - borderRadius: BorderRadius.circular(2), - ), + const SizedBox(height: 8), + // Time range (if not all day) + if (!isAllDay && startTime != null && event.endTime != null) ...[ + _buildDetailRowInline( + icon: Icons.access_time, + value: '${DateFormat('HH:mm').format(startTime.toLocal())} - ${DateFormat('HH:mm').format(event.endTime!.toLocal())}', + accentColor: accentColor, ), - const SizedBox(width: 12), - Expanded( - child: Text( - event.summary ?? 'Event Details', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), + const SizedBox(height: 6), + ], + if (isAllDay) ...[ + _buildDetailRowInline( + icon: Icons.access_time, + value: _t('All day'), + accentColor: accentColor, + ), + const SizedBox(height: 6), + ], + // Location + if (event.location != null && event.location!.isNotEmpty) ...[ + _buildDetailRowInline( + icon: Icons.location_on, + value: event.location!, + accentColor: accentColor, ), ], - ), - const SizedBox(height: 24), - if (startTime != null) ...[ - _buildDetailRow( - icon: Icons.calendar_today, - label: 'Date', - value: DateFormat('EEEE, MMMM d, y').format(startTime), - accentColor: accentColor, - ), - const SizedBox(height: 16), ], - if (!isAllDay && startTime != null && endTime != null) ...[ - _buildDetailRow( - icon: Icons.access_time, - label: 'Time', - value: - '${DateFormat('HH:mm').format(startTime.toLocal())} - ${DateFormat('HH:mm').format(endTime.toLocal())}', - accentColor: accentColor, - ), - const SizedBox(height: 16), - ], - if (isAllDay) ...[ - _buildDetailRow( - icon: Icons.access_time, - label: 'Duration', - value: 'All day event', - accentColor: accentColor, - ), - const SizedBox(height: 16), - ], - if (event.location != null && event.location!.isNotEmpty) ...[ - _buildDetailRow( - icon: Icons.location_on, - label: 'Location', - value: event.location!, - accentColor: accentColor, - ), - const SizedBox(height: 16), - ], - if (event.description != null && event.description!.isNotEmpty) ...[ - _buildDetailRow( - icon: Icons.notes, - label: 'Notes', - value: event.description!, - accentColor: accentColor, - ), - const SizedBox(height: 16), - ], - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: accentColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text( - 'Close', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ), - ), - ], + ), ), - ), + ], ), ); } - Widget _buildDetailRow({ + Widget _buildDetailRowInline({ required IconData icon, - required String label, required String value, required Color accentColor, }) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, size: 20, color: accentColor), - ), - const SizedBox(width: 12), + Icon(icon, size: 16, color: accentColor), + const SizedBox(width: 6), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.grey[600], - ), - ), - const SizedBox(height: 4), - Text( - value, - style: const TextStyle( - fontSize: 14, - color: Colors.black87, - ), - ), - ], + child: Text( + value, + style: const TextStyle( + fontSize: 13, + color: Colors.black87, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], ); } + } diff --git a/lib/screens/camera_screen.dart b/lib/screens/camera_screen.dart index 8301571..a10cea3 100644 --- a/lib/screens/camera_screen.dart +++ b/lib/screens/camera_screen.dart @@ -5,6 +5,7 @@ import 'package:image/image.dart' as img; import 'package:shared_preferences/shared_preferences.dart'; import 'yolo_detector.dart'; import 'preview_screen.dart'; +import '../utils/translations.dart'; class CameraScreen extends StatefulWidget { const CameraScreen({super.key}); @@ -21,6 +22,7 @@ class _CameraScreenState extends State { // The variable to hold the number of bullets entered by the user (default set to 5) int _numberOfBullets = 5; + String _selectedLanguage = 'English'; @override void initState() { @@ -28,9 +30,21 @@ class _CameraScreenState extends State { _loadBulletCount(); _initCamera(); _loadBulletColor(); // 👈 + _loadLanguage(); YOLODetector.create().then((detector) => _yoloDetector = detector); } + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + Future _loadBulletColor() async { final prefs = await SharedPreferences.getInstance(); final selected = prefs.getString('bulletColor') ?? 'Blue'; @@ -168,13 +182,14 @@ Color _getColorFromName(String colorName) { padding: const EdgeInsets.all(24.0), // Increased padding for better spacing child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Padding( - padding: EdgeInsets.only(bottom: 16.0), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), child: Text( - 'Select the number of bullets', - style: TextStyle( + _t('Select the number of bullets'), + textAlign: TextAlign.center, + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -245,9 +260,9 @@ Color _getColorFromName(String colorName) { extendBody: true, appBar: AppBar( backgroundColor: Colors.grey[900], - title: const Text( - "Align the target inside the square", - style: TextStyle( + title: Text( + _t("Align the target inside the square"), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index e5045c5..492f9b8 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -6,6 +6,8 @@ import 'target_screen.dart'; import '../widgets/bottom_nav.dart'; //import 'login_screen.dart'; import 'camera_screen.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../utils/translations.dart'; class HomeScreen extends StatefulWidget { @@ -17,6 +19,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { int _selectedIndex = 1; // Default to Shooting Screen + String _selectedLanguage = 'English'; static final List _pages = [ BookScreen(), @@ -26,7 +29,7 @@ class _HomeScreenState extends State { SettingsScreen(), ]; - static final List _pageTitles = [ + final List _pageTitles = [ 'Book', 'Shooting', 'Scan', @@ -34,6 +37,23 @@ class _HomeScreenState extends State { 'Settings' ]; + @override + void initState() { + super.initState(); + _loadLanguage(); + } + + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + void _onItemTapped(int index) { if (index == 2) { _openCameraScan(); @@ -42,6 +62,8 @@ class _HomeScreenState extends State { setState(() { _selectedIndex = index; }); + // Reload language when switching screens to pick up any changes from Settings + _loadLanguage(); } //void _logout() { @@ -69,7 +91,7 @@ class _HomeScreenState extends State { return Scaffold( appBar: AppBar( title: Text( - _pageTitles[_selectedIndex], + _t(_pageTitles[_selectedIndex]), style: const TextStyle(color: Colors.white), ), backgroundColor: Colors.grey[900], @@ -77,9 +99,18 @@ class _HomeScreenState extends State { automaticallyImplyLeading: false, scrolledUnderElevation: 0, // For Material 3 (Flutter 3.7+) actions: [ - IconButton( - icon: const Icon(Icons.logout, color: Colors.red), - onPressed: null, // disables the button + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Center( + child: Text( + 'V1.0.0', + style: TextStyle( + color: Colors.grey[400], + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), ), ], ), @@ -87,6 +118,7 @@ class _HomeScreenState extends State { bottomNavigationBar: BottomNavBar( selectedIndex: _selectedIndex, onItemTapped: _onItemTapped, + selectedLanguage: _selectedLanguage, ), ); } diff --git a/lib/screens/preview_screen.dart b/lib/screens/preview_screen.dart index 1354bdf..0f84ac6 100644 --- a/lib/screens/preview_screen.dart +++ b/lib/screens/preview_screen.dart @@ -8,6 +8,7 @@ import 'yolo_detector.dart'; import 'dart:convert'; // For JSON encoding import 'dart:math'; import 'package:shared_preferences/shared_preferences.dart'; +import '../utils/translations.dart'; class PreviewScreen extends StatefulWidget { final String imagePath; @@ -56,6 +57,7 @@ class _PreviewScreenState extends State { Color get _activeColor => _isEditMode ? _editColor : _bulletColor; Color get _activeColorWithOpacity => _activeColor.withOpacity(_bulletOpacity); + String _selectedLanguage = 'English'; @override void initState() { @@ -63,9 +65,21 @@ class _PreviewScreenState extends State { _realImageDetections = widget.detections; _targetImageFuture = _loadAndDecodeTargetImage(); _loadBulletColor(); // 👈 Add this + _loadLanguage(); _runDetectionAndMapping(); } + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + Future _loadBulletColor() async { final prefs = await SharedPreferences.getInstance(); final selectedColor = prefs.getString('bulletColor') ?? 'Blue'; @@ -241,9 +255,9 @@ void _manualScoreEntry() { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Enter Missing Bullet Scores', - style: TextStyle( + Text( + _t('Enter Missing Bullet Scores'), + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -263,7 +277,7 @@ void _manualScoreEntry() { keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white), decoration: InputDecoration( - labelText: 'Bullet Score ${_mappedBullets.length + index + 1}', + labelText: '${_t('Bullet Score')} ${_mappedBullets.length + index + 1}', labelStyle: const TextStyle(color: Colors.white54), filled: true, fillColor: Colors.grey[800], @@ -308,7 +322,7 @@ void _manualScoreEntry() { borderRadius: BorderRadius.circular(12), ), ), - child: const Text('Add Bullets'), + child: Text(_t('Add Bullets')), ), ), ], @@ -355,9 +369,10 @@ void _manualScoreEntry() { // Use timestamp for a unique file name final now = DateTime.now(); - final dateString = "${now.year}-${now.month}-${now.day}"; + final dateString = "${now.day.toString().padLeft(2, '0')}/${now.month.toString().padLeft(2, '0')}/${now.year}"; + final dateStringFile = "${now.year}-${now.month}-${now.day}"; final timeString = "${now.hour}-${now.minute}-${now.second}"; - final fileName = "target_${dateString}_$timeString.json"; + final fileName = "target_${dateStringFile}_$timeString.json"; final filePath = '${saveDirectory.path}/$fileName'; // Prepare target data to save @@ -500,20 +515,25 @@ void _moveBullet(Offset offsetDelta) { int trainingCount = 1; int tournamentCount = 1; + // Get localized folder names + final trainingWord = _t('Training'); + final tournamentWord = _t('Tournament'); + for (var folder in folders) { final name = folder.path.split('/').last; - if (name.startsWith('Training')) { - final num = int.tryParse(name.replaceAll('Training ', '')); + // Check for both English and Slovenian names + if (name.startsWith('Training') || name.startsWith('Trening')) { + final num = int.tryParse(name.replaceAll('Training ', '').replaceAll('Trening ', '')); if (num != null && num >= trainingCount) trainingCount = num + 1; } - if (name.startsWith('Tournament')) { - final num = int.tryParse(name.replaceAll('Tournament ', '')); + if (name.startsWith('Tournament') || name.startsWith('Tekmovanje')) { + final num = int.tryParse(name.replaceAll('Tournament ', '').replaceAll('Tekmovanje ', '')); if (num != null && num >= tournamentCount) tournamentCount = num + 1; } } - final String nextTraining = 'Training $trainingCount'; - final String nextTournament = 'Tournament $tournamentCount'; + final String nextTraining = '$trainingWord $trainingCount'; + final String nextTournament = '$tournamentWord $tournamentCount'; return showDialog( context: context, @@ -526,9 +546,9 @@ void _moveBullet(Offset offsetDelta) { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Save Target Data', - style: TextStyle( + Text( + _t('Save Target Data'), + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -536,7 +556,7 @@ void _moveBullet(Offset offsetDelta) { ), const SizedBox(height: 24), - // 🎯 Training Button + // 🎯 Training Button (New folder) SizedBox( width: double.infinity, child: ElevatedButton( @@ -554,12 +574,24 @@ void _moveBullet(Offset offsetDelta) { borderRadius: BorderRadius.circular(12), ), ), - child: Text('🎯 Save to "$nextTraining"'), + child: Column( + children: [ + Text( + '🎯 $nextTraining', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 4), + Text( + _t('New folder'), + style: const TextStyle(fontSize: 12, color: Colors.white70), + ), + ], + ), ), ), const SizedBox(height: 12), - // 🏆 Tournament Button + // 🏆 Tournament Button (New folder) SizedBox( width: double.infinity, child: ElevatedButton( @@ -577,7 +609,19 @@ void _moveBullet(Offset offsetDelta) { borderRadius: BorderRadius.circular(12), ), ), - child: Text('🏆 Save to "$nextTournament"'), + child: Column( + children: [ + Text( + '🏆 $nextTournament', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 4), + Text( + _t('New folder'), + style: const TextStyle(fontSize: 12, color: Colors.white70), + ), + ], + ), ), ), if (lastFolder != null) ...[ @@ -613,13 +657,13 @@ void _moveBullet(Offset offsetDelta) { child: Column( children: [ Text( - '${lastIsTournament ? "🏆" : "🎯"} Save to "$lastFolder"', - style: const TextStyle(fontWeight: FontWeight.bold), + '${lastIsTournament ? "🏆" : "🎯"} $lastFolder', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 4), - const Text( - 'Last used folder', - style: TextStyle(fontSize: 12, color: Colors.white70), + Text( + _t('Last used folder'), + style: const TextStyle(fontSize: 12, color: Colors.white70), ), ], ), @@ -715,9 +759,9 @@ Widget build(BuildContext context) { appBar: AppBar( backgroundColor: Colors.grey[900], iconTheme: const IconThemeData(color: Colors.white), - title: const Text( - 'Detection Preview', - style: TextStyle(color: Colors.white), + title: Text( + _t('Detection Preview'), + style: const TextStyle(color: Colors.white), ), actions: [ IconButton( @@ -735,14 +779,14 @@ Widget build(BuildContext context) { }); }, tooltip: _mode == PreviewMode.mappedTarget - ? "Show Camera Detections" - : "Show Mapped Target", + ? _t("Show Camera Detections") + : _t("Show Mapped Target"), ), IconButton( icon: const Icon(Icons.save), color: Colors.white, onPressed: _saveTargetData, - tooltip: "Save Target Data", + tooltip: _t("Save Target Data"), ), ], ), @@ -1045,17 +1089,51 @@ Widget build(BuildContext context) { ), bottomNavigationBar: BottomAppBar( color: Colors.grey[900], + padding: EdgeInsets.zero, child: SizedBox( - height: 56, - child: Center( - child: Text( - "🎯 Total Score: $_totalScore", - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, + height: 150, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Left side - Label (darker grey) + const Padding( + padding: EdgeInsets.only(right: 16), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "🎯 Total Score:", + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), ), - ), + // Right side - Score badge (grey with rounded corners) + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.grey[700]!, + width: 2, + ), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "$_totalScore", + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], ), ), ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 2e26a31..cbd4741 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../utils/translations.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -40,16 +41,26 @@ class _SettingsScreenState extends State { 'Grey', ]; + String _selectedLanguage = 'English'; + final List> _languages = [ + {'code': 'en', 'name': 'English', 'flag': '🇬🇧'}, + {'code': 'sl', 'name': 'Slovenian', 'flag': '🇸🇮'}, + ]; + @override void initState() { super.initState(); _loadCameraIPs(); _loadBulletColor(); - _loadBulletColor(); + _loadLanguage(); } + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + Color _getColorFromName(String colorName) { switch (colorName.toLowerCase()) { case 'blue': @@ -108,6 +119,21 @@ Future _saveBulletColor(String color) async { setState(() => _selectedBulletColor = color); } +Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + String savedLanguage = prefs.getString('language') ?? 'English'; + + setState(() { + _selectedLanguage = savedLanguage; + }); +} + +Future _saveLanguage(String language) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('language', language); + setState(() => _selectedLanguage = language); +} + Future _loadCameraIPs() async { final prefs = await SharedPreferences.getInstance(); @@ -164,7 +190,7 @@ Future _saveBulletColor(String color) async { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - "Camera ${index + 1}", + "${_t('Camera')} ${index + 1}", style: const TextStyle( color: Colors.white, fontSize: 20, @@ -183,7 +209,7 @@ Future _saveBulletColor(String color) async { decoration: InputDecoration( filled: true, fillColor: Colors.grey[800], - hintText: "Enter Camera IP", + hintText: _t("Enter Camera IP"), hintStyle: const TextStyle(color: Colors.white54), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), enabledBorder: OutlineInputBorder( @@ -207,7 +233,9 @@ Future _saveBulletColor(String color) async { String newIP = controller.text.trim(); if (newIP.isNotEmpty) { await _saveCameraIP(index, newIP); - Navigator.pop(context); + if (context.mounted) { + Navigator.pop(context); + } } }, style: ElevatedButton.styleFrom( @@ -218,7 +246,7 @@ Future _saveBulletColor(String color) async { borderRadius: BorderRadius.circular(12), ), ), - child: const Text("Save"), + child: Text(_t("Save")), ), ), const SizedBox(height: 12), @@ -229,7 +257,9 @@ Future _saveBulletColor(String color) async { child: ElevatedButton( onPressed: () async { await _clearCameraIP(index); - Navigator.pop(context); + if (context.mounted) { + Navigator.pop(context); + } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[700], @@ -239,7 +269,7 @@ Future _saveBulletColor(String color) async { borderRadius: BorderRadius.circular(12), ), ), - child: const Text("Reset to Default"), + child: Text(_t("Reset to Default")), ), ), const SizedBox(height: 12), @@ -257,7 +287,7 @@ Future _saveBulletColor(String color) async { borderRadius: BorderRadius.circular(12), ), ), - child: const Text("Cancel"), + child: Text(_t("Cancel")), ), ), ], @@ -280,6 +310,8 @@ Future _saveBulletColor(String color) async { child: Column( children: [ const SizedBox(height: 8), + _buildLanguageSelector(), + const SizedBox(height: 16), _buildBulletColorSelector(), const SizedBox(height: 16), @@ -290,13 +322,61 @@ Future _saveBulletColor(String color) async { ); } + Widget _buildLanguageSelector() { + final selectedLang = _languages.firstWhere( + (lang) => lang['name'] == _selectedLanguage, + orElse: () => _languages[0], + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _t("Language"), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: _showLanguagePopup, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Text( + selectedLang['flag'] ?? '🌐', + style: const TextStyle(fontSize: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _t(selectedLang['name'] ?? 'English'), + style: const TextStyle(color: Colors.black87, fontSize: 14), + ), + ), + const Icon(Icons.chevron_right, color: Colors.black54), + ], + ), + ), + ), + ], + ); + } + Widget _buildBulletColorSelector() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "Select Bullet Color", - style: TextStyle( + Text( + _t("Select Bullet Color"), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87, @@ -325,7 +405,7 @@ Future _saveBulletColor(String color) async { ), Expanded( child: Text( - _selectedBulletColor, + _t(_selectedBulletColor), style: const TextStyle(color: Colors.black87, fontSize: 14), ), ), @@ -338,6 +418,82 @@ Future _saveBulletColor(String color) async { ); } +void _showLanguagePopup() { + showDialog( + context: context, + builder: (context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _t("Select Language"), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 20), + ..._languages.map((lang) { + final isSelected = lang['name'] == _selectedLanguage; + + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + await _saveLanguage(lang['name']!); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: isSelected + ? _getColorFromName(_selectedBulletColor) + : Colors.grey[800], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + lang['flag']!, + style: const TextStyle(fontSize: 20), + ), + const SizedBox(width: 12), + Text( + _t(lang['name']!), + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ); + }), + const SizedBox(height: 8), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(_t("Cancel"), style: const TextStyle(color: Colors.white70)), + ), + ], + ), + ), + ); + }, + ); +} + void _showBulletColorPopup() { showDialog( context: context, @@ -350,9 +506,9 @@ void _showBulletColorPopup() { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - "Select Bullet Color", - style: TextStyle( + Text( + _t("Select Bullet Color"), + style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, @@ -369,7 +525,9 @@ void _showBulletColorPopup() { return GestureDetector( onTap: () async { await _saveBulletColor(colorName); - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, child: Container( width: 48, @@ -389,7 +547,7 @@ void _showBulletColorPopup() { const SizedBox(height: 20), TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text("Cancel", style: TextStyle(color: Colors.white70)), + child: Text(_t("Cancel"), style: const TextStyle(color: Colors.white70)), ), ], ), @@ -403,9 +561,9 @@ void _showBulletColorPopup() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "Camera Settings", - style: TextStyle( + Text( + _t("Camera Settings"), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87, @@ -429,7 +587,7 @@ void _showBulletColorPopup() { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Camera ${index + 1}", + "${_t('Camera')} ${index + 1}", style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.black87, diff --git a/lib/screens/stream_screen.dart b/lib/screens/stream_screen.dart index 24d92fd..964d40d 100644 --- a/lib/screens/stream_screen.dart +++ b/lib/screens/stream_screen.dart @@ -9,6 +9,7 @@ import 'package:image/image.dart' as img; import 'yolo_detector.dart'; import 'preview_screen.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import '../utils/translations.dart'; enum ImageFilterType { none, greyscale, invert} @@ -38,10 +39,16 @@ class _CameraStreamScreenState extends State with SingleTick Matrix4? _originalMatrix; Color _bulletColor = Colors.blue; // default/fallback bullet color + String _selectedLanguage = 'English'; + + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } @override void initState() { super.initState(); + _loadLanguage(); _loadStreamUrl(); _loadBulletCount(); _loadSelectedFilter(); @@ -61,6 +68,13 @@ class _CameraStreamScreenState extends State with SingleTick ); } + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + Future _loadBulletColor() async { final prefs = await SharedPreferences.getInstance(); final selected = prefs.getString('bulletColor') ?? 'Blue'; @@ -297,13 +311,14 @@ Color _getColorFromName(String colorName) { padding: const EdgeInsets.all(24.0), // Increased padding for better spacing child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Padding( - padding: EdgeInsets.only(bottom: 16.0), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), child: Text( - 'Select the number of bullets', - style: TextStyle( + _t('Select the number of bullets'), + textAlign: TextAlign.center, + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -365,9 +380,9 @@ Color _getColorFromName(String colorName) { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Choose a Filter', - style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), + Text( + _t('Choose a Filter'), + style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Wrap( @@ -377,6 +392,7 @@ Color _getColorFromName(String colorName) { children: ImageFilterType.values.map((f) { final isSelected = _selectedFilter == f; final label = f.toString().split('.').last; + final translatedLabel = _t(label[0].toUpperCase() + label.substring(1)); return ElevatedButton( onPressed: () async { setState(() { @@ -384,13 +400,15 @@ Color _getColorFromName(String colorName) { }); final prefs = await SharedPreferences.getInstance(); await prefs.setInt('selectedFilter', f.index); // Save filter index - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, style: ElevatedButton.styleFrom( backgroundColor: isSelected ? _bulletColor : Colors.grey[800], foregroundColor: Colors.white, ), - child: Text(label[0].toUpperCase() + label.substring(1)), + child: Text(translatedLabel), ); }).toList(), ), @@ -455,7 +473,7 @@ void dispose() { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Target ${widget.cameraIndex}"), + title: Text("${_t('Camera')} ${widget.cameraIndex}"), backgroundColor: Colors.grey[900], foregroundColor: Colors.white, leading: IconButton( @@ -523,14 +541,14 @@ void dispose() { Positioned.fill( child: Container( color: Colors.black.withOpacity(0.4), - child: const Column( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(color: Colors.white), - SizedBox(height: 16), + const CircularProgressIndicator(color: Colors.white), + const SizedBox(height: 16), Text( - "Processing image...", - style: TextStyle(color: Colors.white, fontSize: 16), + _t("Processing image..."), + style: const TextStyle(color: Colors.white, fontSize: 16), ), ], ), @@ -571,14 +589,14 @@ void dispose() { children: [ const Icon(Icons.error, color: Colors.red, size: 100), const SizedBox(height: 20), - const Text( - "Unable to load camera stream", - style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + Text( + _t("Unable to load camera stream"), + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), ElevatedButton( onPressed: _loadStreamUrl, - child: const Text("Retry"), + child: Text(_t("Retry")), ), ], ); diff --git a/lib/screens/target_screen.dart b/lib/screens/target_screen.dart index 9ab7272..15e0b11 100644 --- a/lib/screens/target_screen.dart +++ b/lib/screens/target_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'stream_screen.dart'; // Import the camera stream screen +import 'stream_screen.dart'; // Import the camera stream screen +import '../utils/translations.dart'; class TargetScreen extends StatefulWidget { const TargetScreen({super.key}); @@ -10,109 +11,124 @@ class TargetScreen extends StatefulWidget { } class _TargetScreenState extends State { - List _cameraButtonsEnabled = List.generate(6, (_) => false); // Initially, all buttons are disabled + List _cameraButtonsEnabled = + List.generate(6, (_) => false); // Initially, all buttons are disabled + String _selectedLanguage = 'English'; @override void initState() { super.initState(); + _loadLanguage(); _loadCameraData(); } + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + /// Load the camera IPs from SharedPreferences and update button status Future _loadCameraData() async { - final prefs = await SharedPreferences.getInstance(); - final List defaultIPs = [ - '192.168.0.134:9081', - '192.168.0.134:9082', - '192.168.0.134:9083', - '192.168.0.134:9084', - '192.168.0.134:9085', - '192.168.0.134:9086', - ]; + final prefs = await SharedPreferences.getInstance(); + final List defaultIPs = [ + '192.168.0.134:9081', + '192.168.0.134:9082', + '192.168.0.134:9083', + '192.168.0.134:9084', + '192.168.0.134:9085', + '192.168.0.134:9086', + ]; - List cameraDataStatus = List.generate(6, (index) { - String key = 'cameraIP_${index + 1}'; + List cameraDataStatus = List.generate(6, (index) { + String key = 'cameraIP_${index + 1}'; - if (!prefs.containsKey(key)) { - prefs.setString(key, defaultIPs[index]); - return true; // Default set - } + if (!prefs.containsKey(key)) { + prefs.setString(key, defaultIPs[index]); + return true; // Default set + } - String? ip = prefs.getString(key); - return ip != null && ip.isNotEmpty && ip != 'Not Set'; - }); + String? ip = prefs.getString(key); + return ip != null && ip.isNotEmpty && ip != 'Not Set'; + }); - if (!mounted) return; - setState(() { - _cameraButtonsEnabled = cameraDataStatus; - }); -} + if (!mounted) return; + setState(() { + _cameraButtonsEnabled = cameraDataStatus; + }); + } @override Widget build(BuildContext context) { - return Scaffold( - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), - child: Column( - children: [ - Expanded( - child: GridView.builder( - itemCount: 6, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: 1, // square-ish buttons - ), - itemBuilder: (context, index) { - final isEnabled = _cameraButtonsEnabled[index]; - return InkWell( - onTap: isEnabled - ? () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CameraStreamScreen(cameraIndex: index + 1), - ), - ); - } - : null, - borderRadius: BorderRadius.circular(16), - child: Container( - decoration: BoxDecoration( - color: isEnabled ? Colors.grey[900] : Colors.grey[300], - borderRadius: BorderRadius.circular(16), - ), - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.videocam_rounded, - color: isEnabled ? Colors.white : Colors.grey[500], - size: 36, - ), - const SizedBox(height: 12), - Text( - "Target ${index + 1}", - textAlign: TextAlign.center, - style: TextStyle( - color: isEnabled ? Colors.white : Colors.grey[600], - fontSize: 20, - fontWeight: FontWeight.w600, + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + children: [ + Expanded( + child: GridView.builder( + itemCount: 6, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1, // square-ish buttons + ), + itemBuilder: (context, index) { + final isEnabled = _cameraButtonsEnabled[index]; + return InkWell( + onTap: isEnabled + ? () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + CameraStreamScreen(cameraIndex: index + 1), + ), + ); + } + : null, + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + color: isEnabled ? Colors.grey[900] : Colors.grey[300], + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.videocam_rounded, + color: isEnabled ? Colors.white : Colors.grey[500], + size: 36, ), - ), - ], + const SizedBox(height: 12), + Text( + "${_t('Camera')} ${index + 1}", + textAlign: TextAlign.center, + style: TextStyle( + color: + isEnabled ? Colors.white : Colors.grey[600], + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - ], + ], + ), ), - ), - ); -} - + ); + } } diff --git a/lib/screens/target_view.dart b/lib/screens/target_view.dart index b894ebb..212f588 100644 --- a/lib/screens/target_view.dart +++ b/lib/screens/target_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart' show rootBundle; import 'dart:convert'; import 'package:image/image.dart' as img; import 'package:shared_preferences/shared_preferences.dart'; +import '../utils/translations.dart'; class TargetViewScreen extends StatefulWidget { final List jsonFiles; @@ -38,15 +39,28 @@ class _TargetViewScreenState extends State { final double bulletSize = 19; Color _bulletColor = Colors.blue; // default/fallback bullet color + String _selectedLanguage = 'English'; @override void initState() { super.initState(); - _loadBulletColor(); + _loadBulletColor(); + _loadLanguage(); _loadJsonAndImage(); } + String _t(String key) { + return AppTranslations.translate(key, _selectedLanguage); + } + + Future _loadLanguage() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLanguage = prefs.getString('language') ?? 'English'; + }); + } + Future _loadBulletColor() async { final prefs = await SharedPreferences.getInstance(); final selectedColor = prefs.getString('bulletColor') ?? 'Blue'; @@ -116,7 +130,25 @@ class _TargetViewScreenState extends State { final jsonString = await currentFile.readAsString(); final data = jsonDecode(jsonString); - _date = data['date'] ?? 'Unknown Date'; + // Parse and convert date format from yyyy-m-d to dd/MM/yyyy + String rawDate = data['date'] ?? 'Unknown Date'; + if (rawDate != 'Unknown Date' && rawDate.contains('-')) { + try { + final parts = rawDate.split('-'); + if (parts.length == 3) { + final year = parts[0]; + final month = parts[1].padLeft(2, '0'); + final day = parts[2].padLeft(2, '0'); + _date = '$day/$month/$year'; + } else { + _date = rawDate; + } + } catch (e) { + _date = rawDate; + } + } else { + _date = rawDate; + } final bulletsJson = data['bullets'] as List; _bullets = bulletsJson.map((b) => Offset(b['x'], b['y'])).toList(); @@ -185,9 +217,9 @@ class _TargetViewScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( - 'Delete Target', - style: TextStyle( + Text( + _t('Delete Target'), + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -195,9 +227,9 @@ class _TargetViewScreenState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 16), - const Text( - "Are you sure you want to delete this target?", - style: TextStyle(color: Colors.white70, fontSize: 16), + Text( + _t("Are you sure you want to delete this target?"), + style: const TextStyle(color: Colors.white70, fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 24), @@ -218,7 +250,7 @@ class _TargetViewScreenState extends State { borderRadius: BorderRadius.circular(12), ), ), - child: const Text("Yes, delete"), + child: Text(_t("Yes, delete")), ), ), const SizedBox(height: 12), @@ -339,17 +371,36 @@ class _TargetViewScreenState extends State { ), bottomNavigationBar: BottomAppBar( color: Colors.grey[900], + padding: EdgeInsets.zero, child: SizedBox( - height: 56, - child: Center( - child: Text( - "🎯 Total Score: $_totalScore", - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.white, + height: 150, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Combined badge with text and score + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.grey[700]!, + width: 2, + ), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "🎯 ${_t('Total score')} $_totalScore", + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), ), - ), + ], ), ), ), diff --git a/lib/utils/translations.dart b/lib/utils/translations.dart new file mode 100644 index 0000000..9124f0f --- /dev/null +++ b/lib/utils/translations.dart @@ -0,0 +1,379 @@ +class AppTranslations { + static Map> translations = { + // Language names + 'Language': { + 'English': 'Language', + 'Slovenian': 'Jezik', + }, + 'Select Language': { + 'English': 'Select Language', + 'Slovenian': 'Izberi jezik', + }, + 'English': { + 'English': 'English', + 'Slovenian': 'Angleščina', + }, + 'Slovenian': { + 'English': 'Slovenian', + 'Slovenian': 'Slovenščina', + }, + + // Bullet color + 'Select Bullet Color': { + 'English': 'Select Bullet Color', + 'Slovenian': 'Izberi barvo zadetka', + }, + + // Camera settings + 'Camera Settings': { + 'English': 'Camera Settings', + 'Slovenian': 'Nastavitve kamere', + }, + 'Camera': { + 'English': 'Camera', + 'Slovenian': 'Kamera', + }, + 'Enter Camera IP': { + 'English': 'Enter Camera IP', + 'Slovenian': 'Vnesi IP kamere', + }, + 'Save': { + 'English': 'Save', + 'Slovenian': 'Shrani', + }, + 'Reset to Default': { + 'English': 'Reset to Default', + 'Slovenian': 'Ponastavi na privzeto', + }, + 'Cancel': { + 'English': 'Cancel', + 'Slovenian': 'Prekliči', + }, + + // Color names + 'Blue': { + 'English': 'Blue', + 'Slovenian': 'Modra', + }, + 'Green': { + 'English': 'Green', + 'Slovenian': 'Zelena', + }, + 'Yellow': { + 'English': 'Yellow', + 'Slovenian': 'Rumena', + }, + 'Purple': { + 'English': 'Purple', + 'Slovenian': 'Vijolična', + }, + 'Cyan': { + 'English': 'Cyan', + 'Slovenian': 'Cian', + }, + 'Teal': { + 'English': 'Teal', + 'Slovenian': 'Modrozelena', + }, + 'Indigo': { + 'English': 'Indigo', + 'Slovenian': 'Indigo', + }, + 'Pink': { + 'English': 'Pink', + 'Slovenian': 'Rožnata', + }, + 'Lime': { + 'English': 'Lime', + 'Slovenian': 'Limona', + }, + 'Mint': { + 'English': 'Mint', + 'Slovenian': 'Meta', + }, + 'Orange': { + 'English': 'Orange', + 'Slovenian': 'Oranžna', + }, + 'Violet': { + 'English': 'Violet', + 'Slovenian': 'Vijolična', + }, + 'Amber': { + 'English': 'Amber', + 'Slovenian': 'Jantarna', + }, + 'Brown': { + 'English': 'Brown', + 'Slovenian': 'Rjava', + }, + 'Grey': { + 'English': 'Grey', + 'Slovenian': 'Siva', + }, + + // Calendar Screen + 'Retry': { + 'English': 'Retry', + 'Slovenian': 'Poskusi znova', + }, + 'No upcoming events': { + 'English': 'No upcoming events', + 'Slovenian': 'Ni prihajajočih dogodkov', + }, + 'Pull down to refresh': { + 'English': 'Pull down to refresh', + 'Slovenian': 'Potegni navzdol za osvežitev', + }, + 'Today': { + 'English': 'Today', + 'Slovenian': 'Danes', + }, + 'Tomorrow': { + 'English': 'Tomorrow', + 'Slovenian': 'Jutri', + }, + 'NOW': { + 'English': 'NOW', + 'Slovenian': 'ZDAJ', + }, + 'Untitled Event': { + 'English': 'Untitled Event', + 'Slovenian': 'Brez naslova', + }, + 'All day': { + 'English': 'All day', + 'Slovenian': 'Cel dan', + }, + + // Preview Screen + 'Enter Missing Bullet Scores': { + 'English': 'Enter Missing Bullet Scores', + 'Slovenian': 'Vnesi manjkajoče ocene zadetkov', + }, + 'Bullet Score': { + 'English': 'Bullet Score', + 'Slovenian': 'Ocena zadetka', + }, + 'Add Bullets': { + 'English': 'Add Bullets', + 'Slovenian': 'Dodaj zadetke', + }, + 'Save Target Data': { + 'English': 'Save Target Data', + 'Slovenian': 'Shrani podatke tarče', + }, + 'Training': { + 'English': 'Training', + 'Slovenian': 'Trening', + }, + 'Tournament': { + 'English': 'Tournament', + 'Slovenian': 'Tekmovanje', + }, + 'Last used folder': { + 'English': 'Last used folder', + 'Slovenian': 'Zadnja uporabljena mapa', + }, + 'New folder': { + 'English': 'New folder', + 'Slovenian': 'Nova mapa', + }, + 'Detection Preview': { + 'English': 'Detection Preview', + 'Slovenian': 'Predogled zaznave', + }, + 'Show Camera Detections': { + 'English': 'Show Camera Detections', + 'Slovenian': 'Prikaži zaznave kamere', + }, + 'Show Mapped Target': { + 'English': 'Show Mapped Target', + 'Slovenian': 'Prikaži preslikano tarčo', + }, + + // Book Screen + 'Folder Settings': { + 'English': 'Folder Settings', + 'Slovenian': 'Nastavitve mape', + }, + 'Enter new folder name': { + 'English': 'Enter new folder name', + 'Slovenian': 'Vnesi novo ime mape', + }, + 'Tournament Mode 🏆': { + 'English': 'Tournament Mode 🏆', + 'Slovenian': 'Tekmovalni način 🏆', + }, + 'Mark as Tournament 🏆': { + 'English': 'Mark as Tournament 🏆', + 'Slovenian': 'Označi kot tekmovanje 🏆', + }, + 'Apply': { + 'English': 'Apply', + 'Slovenian': 'Uporabi', + }, + 'Confirm Deletion': { + 'English': 'Confirm Deletion', + 'Slovenian': 'Potrdi brisanje', + }, + 'Yes, delete': { + 'English': 'Yes, delete', + 'Slovenian': 'Da, izbriši', + }, + 'No targets in this folder': { + 'English': 'No targets in this folder', + 'Slovenian': 'V tej mapi ni tarč', + }, + 'Target': { + 'English': 'Target', + 'Slovenian': 'Tarča', + }, + 'Close': { + 'English': 'Close', + 'Slovenian': 'Zapri', + }, + 'Showing: 📂 All folders': { + 'English': 'Showing: 📂 All folders', + 'Slovenian': 'Prikaz: 📂 Vse mape', + }, + 'Showing: 🏆 Tournament folders': { + 'English': 'Showing: 🏆 Tournament folders', + 'Slovenian': 'Prikaz: 🏆 Tekmovalne mape', + }, + 'Showing: 🎯 Training folders': { + 'English': 'Showing: 🎯 Training folders', + 'Slovenian': 'Prikaz: 🎯 Treningske mape', + }, + 'No saved target data found': { + 'English': 'No saved target data found', + 'Slovenian': 'Ni shranjenih podatkov tarče', + }, + 'Filter Folders': { + 'English': 'Filter Folders', + 'Slovenian': 'Filtriraj mape', + }, + '📂 All folders': { + 'English': '📂 All folders', + 'Slovenian': '📂 Vse mape', + }, + '🏆 Tournament': { + 'English': '🏆 Tournament', + 'Slovenian': '🏆 Tekmovanje', + }, + '🎯 Training': { + 'English': '🎯 Training', + 'Slovenian': '🎯 Trening', + }, + 'Sort By': { + 'English': 'Sort By', + 'Slovenian': 'Razvrsti po', + }, + '📅 Sort by Date': { + 'English': '📅 Sort by Date', + 'Slovenian': '📅 Razvrsti po datumu', + }, + '🏆 Sort by Score': { + 'English': '🏆 Sort by Score', + 'Slovenian': '🏆 Razvrsti po oceni', + }, + + // Navigation Menu + 'Book': { + 'English': 'Book', + 'Slovenian': 'Knjiga', + }, + 'Shooting': { + 'English': 'Shooting', + 'Slovenian': 'Streljanje', + }, + 'Calendar': { + 'English': 'Calendar', + 'Slovenian': 'Koledar', + }, + 'Settings': { + 'English': 'Settings', + 'Slovenian': 'Nastavitve', + }, + 'Scan': { + 'English': 'Scan', + 'Slovenian': 'Skeniraj', + }, + + // Camera Screen + 'Align the target inside the square': { + 'English': 'Align the target inside the square', + 'Slovenian': 'Poravnaj tarčo znotraj kvadrata', + }, + 'Select the number of bullets': { + 'English': 'Select the number of bullets', + 'Slovenian': 'Izberi število zadetkov', + }, + + // Book Screen - Preview + 'Total score': { + 'English': 'Total score', + 'Slovenian': 'Rezultat:', + }, + + // Stream Screen (Camera View) + 'Choose a Filter': { + 'English': 'Choose a Filter', + 'Slovenian': 'Izberi filter', + }, + 'None': { + 'English': 'None', + 'Slovenian': 'Brez', + }, + 'Greyscale': { + 'English': 'Greyscale', + 'Slovenian': 'Sivine', + }, + 'Invert': { + 'English': 'Invert', + 'Slovenian': 'Inverz', + }, + 'Processing image...': { + 'English': 'Processing image...', + 'Slovenian': 'Obdelava slike...', + }, + 'Unable to load camera stream': { + 'English': 'Unable to load camera stream', + 'Slovenian': 'Ni mogoče naložiti kamere', + }, + + // Delete confirmations + 'Are you sure you want to delete this file?': { + 'English': 'Are you sure you want to delete this file?', + 'Slovenian': 'Ali ste prepričani, da želite izbrisati to datoteko?', + }, + 'Are you sure you want to delete this target?': { + 'English': 'Are you sure you want to delete this target?', + 'Slovenian': 'Ali ste prepričani, da želite izbrisati to tarčo?', + }, + 'Delete Target': { + 'English': 'Delete Target', + 'Slovenian': 'Izbriši tarčo', + }, + 'Yes, delete': { + 'English': 'Yes, delete', + 'Slovenian': 'Da, izbriši', + }, + 'Are you sure you want to delete the folder': { + 'English': 'Are you sure you want to delete the folder', + 'Slovenian': 'Ali ste prepričani, da želite izbrisati mapo', + }, + 'and all of its contents?': { + 'English': 'and all of its contents?', + 'Slovenian': 'in vso njeno vsebino?', + }, + }; + + static String translate(String key, String language) { + if (translations.containsKey(key) && + translations[key]!.containsKey(language)) { + return translations[key]![language]!; + } + return key; // Return the key itself if translation not found + } +} diff --git a/lib/widgets/bottom_nav.dart b/lib/widgets/bottom_nav.dart index 70e0b01..f16137c 100644 --- a/lib/widgets/bottom_nav.dart +++ b/lib/widgets/bottom_nav.dart @@ -1,15 +1,22 @@ import 'package:flutter/material.dart'; +import '../utils/translations.dart'; class BottomNavBar extends StatelessWidget { final int selectedIndex; final Function(int) onItemTapped; + final String selectedLanguage; const BottomNavBar({ super.key, required this.selectedIndex, required this.onItemTapped, + required this.selectedLanguage, }); + String _t(String key) { + return AppTranslations.translate(key, selectedLanguage); + } + @override Widget build(BuildContext context) { return SafeArea( @@ -26,12 +33,12 @@ Widget build(BuildContext context) { unselectedFontSize: 12, selectedItemColor: Colors.white, unselectedItemColor: Colors.white70, - items: const [ - BottomNavigationBarItem(icon: Icon(Icons.book), label: "Book"), - BottomNavigationBarItem(icon: Icon(Icons.my_location), label: "Shooting"), - BottomNavigationBarItem(icon: Icon(Icons.camera_alt), label: "Camera"), - BottomNavigationBarItem(icon: Icon(Icons.calendar_today), label: "Calendar"), - BottomNavigationBarItem(icon: Icon(Icons.settings), label: "Settings"), + items: [ + BottomNavigationBarItem(icon: const Icon(Icons.book), label: _t("Book")), + BottomNavigationBarItem(icon: const Icon(Icons.my_location), label: _t("Shooting")), + BottomNavigationBarItem(icon: const Icon(Icons.camera_alt), label: _t("Camera")), + BottomNavigationBarItem(icon: const Icon(Icons.calendar_today), label: _t("Calendar")), + BottomNavigationBarItem(icon: const Icon(Icons.settings), label: _t("Settings")), ], ), ),