Compare commits

..

10 Commits

69 changed files with 5479 additions and 2459 deletions
-200
View File
@@ -1,200 +0,0 @@
# Release Guide - Shooting App
## Overview
This guide explains how to create and publish a new release of the Shooting App. The app uses GitHub releases and semantic versioning for update management.
## Current Setup
- **Repository**: https://github.com/Bl3kiiie/shooting_app
- **Update Method**: Manual check via Settings button (no automatic prompts)
- **Version File**: `pubspec.yaml`
- **Update Detection**: Compares app version with GitHub release tags
---
## Step-by-Step Release Process
### 1. Update Version in pubspec.yaml
Open `pubspec.yaml` and update the version number:
```yaml
version: 1.0.2+2
```
Format: `MAJOR.MINOR.PATCH+BUILD_NUMBER`
Examples:
- `1.0.1+1` → `1.0.2+1` (patch release, new build)
- `1.0.2+1` → `1.0.2+2` (same version, new build)
- `1.0.2+2` → `1.1.0+1` (minor release)
**Important**: New tag must be HIGHER than previous version for users to see update prompt.
### 2. Build Release APK
```bash
# Navigate to project directory
cd /home/bl3kunja/Documents/Programming/Strelišče/shooting_app
# Build release APK
flutter build apk --release
```
The APK will be generated at: `build/app/outputs/flutter-app/release/app-release.apk`
### 3. Create GitHub Release (Option A: Web Interface - Recommended)
1. Go to: https://github.com/Bl3kiiie/shooting_app/releases
2. Click **Draft a new release** or **Create a new release**
3. Fill in the form:
**Tag version**: `v1.0.2` (must match your new version number)
**Release title**: e.g., `Version 1.0.2 - Bug Fixes and Improvements`
**Description** (example):
```
## Changes in v1.0.2
- Fixed camera calibration issue
- Improved bullet detection accuracy
- Added German language support
- Performance optimizations
## Installation
Download the APK from the assets below and install on your device.
```
4. **Upload APK**:
- Click "Attach binaries by dropping them here or selecting them"
- Select: `build/app/outputs/flutter-app/release/app-release.apk`
5. Click **Publish release**
### 3. Create GitHub Release (Option B: Command Line)
If using GitHub CLI (`gh` command):
```bash
# Create tag locally
git tag v1.0.2
# Push tag to GitHub
git push origin v1.0.2
# Create release (interactive)
gh release create v1.0.2 \
--title "Version 1.0.2" \
--notes "Your changelog here"
# Upload APK to release
gh release upload v1.0.2 build/app/outputs/flutter-app/release/app-release.apk
```
### 4. Commit Version Update (Optional)
If you want to track version changes in git:
```bash
git add pubspec.yaml
git commit -m "bump version to 1.0.2+2"
git push origin main
```
---
## How Users Update
1. User opens app → Settings screen
2. User taps **"Check for Updates"** button
3. App compares:
- Current version: `1.0.1+1`
- Latest GitHub release tag: `v1.0.2`
4. Since `v1.0.2 > v1.0.1`, update is available
5. Dialog shows **"Update Available"**
6. User clicks **"Update"** → Opens GitHub release page
7. User downloads and installs new APK
---
## Important Notes
### Version Numbering
- Always increment version number before creating release
- Tag format: `vX.Y.Z` (e.g., `v1.0.2`, `v1.1.0`)
- App checks: `tag_version > app_version`
### GitHub Token
- Stored in: `lib/services/update_checker.dart`
- Token: `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`
- Scopes: `repo` (for private repo access)
- Consider rotating this token periodically for security
### Testing Before Release
```bash
# Test the build
flutter build apk --release
# Verify APK was created
ls -lh build/app/outputs/flutter-app/release/app-release.apk
# Optionally install on connected device
flutter install build/app/outputs/flutter-app/release/app-release.apk
```
### Troubleshooting Update Detection
If users don't see update prompt:
1. Check GitHub release exists: https://github.com/Bl3kiiie/shooting_app/releases
2. Verify tag format is `vX.Y.Z` (must start with 'v')
3. Confirm new version > current version
4. User must tap "Check for Updates" manually
5. Check app logs for errors
---
## Checklist for Next Release
- [ ] Update version in `pubspec.yaml`
- [ ] Test build: `flutter build apk --release`
- [ ] Create GitHub release with tag `vX.Y.Z`
- [ ] Upload APK to release
- [ ] Write changelog/description
- [ ] Publish release
- [ ] (Optional) Test update detection in app
---
## Example Release Workflow
```bash
# 1. Update version in pubspec.yaml (manually edit to 1.0.2+1)
# 2. Build APK
flutter build apk --release
# 3. Create tag and push
git tag v1.0.2
git push origin v1.0.2
# 4. Create release via GitHub web:
# - Go to https://github.com/Bl3kiiie/shooting_app/releases
# - Draft new release
# - Tag: v1.0.2
# - Upload APK from build/app/outputs/flutter-app/release/app-release.apk
# - Publish
# 5. Users can now "Check for Updates" and see the new version
```
---
## Files Referenced
- `pubspec.yaml` - Version number
- `lib/services/update_checker.dart` - GitHub token and update logic
- `lib/screens/settings_screen.dart` - "Check for Updates" button
- `lib/utils/translations.dart` - Update dialog translations
---
**Last Updated**: November 13, 2025
**Current Version**: 1.0.1
**Next Planned Version**: 1.0.2
Binary file not shown.
+8 -1
View File
@@ -14,6 +14,7 @@ android {
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
} }
kotlinOptions { kotlinOptions {
@@ -22,7 +23,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.example.shooter_app" applicationId = "com.example.shooter_app"
minSdk = 26 // ✅ From earlier fix minSdk = 26
targetSdk = 33 targetSdk = 33
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
@@ -31,12 +32,18 @@ android {
buildTypes { buildTypes {
release { release {
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = false
isShrinkResources = false
} }
} }
} }
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter { flutter {
source = "../.." source = "../.."
} }
+10
View File
@@ -16,3 +16,13 @@
# Keep your YOLO model loader class if needed # Keep your YOLO model loader class if needed
-keep class your.package.name.YOLODetect { *; } -keep class your.package.name.YOLODetect { *; }
# Flutter Local Notifications
-keep class com.dexterous.** { *; }
-dontwarn com.dexterous.**
# Timezone
-keep class com.jakewharton.threetenabp.** { *; }
-dontwarn com.jakewharton.threetenabp.**
-keep class org.threeten.** { *; }
-dontwarn org.threeten.**
+10
View File
@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application <application
@@ -41,6 +42,15 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -0,0 +1,42 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Outer circle -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:pathData="M12,2 A10,10 0 1,1 11.999,2 Z"/>
<!-- Middle circle -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:pathData="M12,6 A6,6 0 1,1 11.999,6 Z"/>
<!-- Inner circle -->
<path
android:fillColor="#FFFFFF"
android:pathData="M12,9 A3,3 0 1,1 11.999,9 Z"/>
<!-- Crosshair top -->
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:pathData="M12,2 L12,0"/>
<!-- Crosshair bottom -->
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:pathData="M12,22 L12,24"/>
<!-- Crosshair left -->
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:pathData="M2,12 L0,12"/>
<!-- Crosshair right -->
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:pathData="M22,12 L24,12"/>
</vector>
+1 -1
View File
@@ -19,7 +19,7 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false
} }
include(":app") include(":app")
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

+14
View File
@@ -1,9 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Import for SystemChrome import 'package:flutter/services.dart'; // Import for SystemChrome
import 'screens/home_screen.dart'; import 'screens/home_screen.dart';
import 'services/notification_service.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
try {
await NotificationService().initialize();
} catch (_) {}
// Lock orientation to portrait only // Lock orientation to portrait only
await SystemChrome.setPreferredOrientations([ await SystemChrome.setPreferredOrientations([
@@ -46,6 +50,16 @@ class MyApp extends StatelessWidget {
backgroundColor: Colors.grey[900], backgroundColor: Colors.grey[900],
), ),
), ),
builder: (context, child) {
final originalTextScaler = MediaQuery.of(context).textScaler;
final clampedScale = originalTextScaler.clamp(minScaleFactor: 0.8, maxScaleFactor: 1.2);
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: clampedScale,
),
child: child!,
);
},
home: const HomeScreen(), home: const HomeScreen(),
); );
} }
+126 -75
View File
@@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart'; // For getApplicationDocument
import 'target_view.dart'; // Import the TargetViewScreen import 'target_view.dart'; // Import the TargetViewScreen
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../utils/translations.dart'; import '../utils/translations.dart';
import '../utils/target_colors.dart';
class BookScreen extends StatefulWidget { class BookScreen extends StatefulWidget {
const BookScreen({super.key}); const BookScreen({super.key});
@@ -25,13 +26,17 @@ class _BookScreenState extends State<BookScreen> with WidgetsBindingObserver {
String _sortMode = 'date'; // or 'score' String _sortMode = 'date'; // or 'score'
String _selectedLanguage = 'English'; String _selectedLanguage = 'English';
String? _expandedFolder; // Track which folder is expanded String? _expandedFolder; // Track which folder is expanded
late Color _backgroundColor;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); // Register to listen for lifecycle events WidgetsBinding.instance.addObserver(this); // Register to listen for lifecycle events
// Initialize with default target background color
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
_loadBulletColor(); _loadBulletColor();
_loadLanguage(); _loadLanguage();
_loadSelectedTargetColor();
_loadTargetDataFiles(); // Initial load when the screen is first created _loadTargetDataFiles(); // Initial load when the screen is first created
} }
@@ -46,6 +51,14 @@ class _BookScreenState extends State<BookScreen> with WidgetsBindingObserver {
}); });
} }
Future<void> _loadSelectedTargetColor() async {
final prefs = await SharedPreferences.getInstance();
final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png';
setState(() {
_backgroundColor = TargetColors.getBackgroundColor(selectedTarget);
});
}
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); // Unregister from lifecycle events when disposed WidgetsBinding.instance.removeObserver(this); // Unregister from lifecycle events when disposed
@@ -214,7 +227,7 @@ Color _getColorFromName(String colorName) {
await file.delete(); await file.delete();
_loadTargetDataFiles(); // Refresh after deleting the file _loadTargetDataFiles(); // Refresh after deleting the file
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Error deleting file: $e"))); debugPrint("Error deleting file: $e");
} }
} }
} }
@@ -236,43 +249,14 @@ Color _getColorFromName(String colorName) {
void _openFolderTargetView(String folderName) async { void _openFolderTargetView(String folderName) async {
final targetFiles = _groupedFiles[folderName] ?? []; final targetFiles = _groupedFiles[folderName] ?? [];
List<Map<String, dynamic>> allBullets = []; // Navigate to view screen with all individual files
String latestDate = 'Unknown';
for (var file in targetFiles) {
final jsonString = await file.readAsString();
final data = jsonDecode(jsonString);
final bullets = (data['bullets'] as List)
.map((b) => {'x': b['x'], 'y': b['y']})
.toList();
allBullets.addAll(bullets);
if (data['date'] != null) {
latestDate = data['date']; // You could sort if you want the most recent
}
}
// Create combined JSON structure
final combinedJson = jsonEncode({
'date': latestDate,
'bullets': allBullets,
});
// Save this JSON temporarily to disk
final tempDir = await getTemporaryDirectory();
final combinedFile = File('${tempDir.path}/combined_${folderName}.json');
await combinedFile.writeAsString(combinedJson);
// Navigate to view screen with the combined file
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TargetViewScreen( builder: (context) => TargetViewScreen(
jsonFiles: [combinedFile], // treat it like a normal file jsonFiles: targetFiles, // Pass all individual files
refreshFolders: _loadTargetDataFiles, refreshFolders: _loadTargetDataFiles,
isCombinedView: true, // 👈 This line isCombinedView: true, // 👈 This signals combined/folder view
), ),
), ),
); );
@@ -367,7 +351,7 @@ Future<void> _renameFolder(String oldFolderName) async {
child: Container( child: Container(
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isTournament ? _bulletColor.withOpacity(0.2) : Colors.grey[850], color: isTournament ? _bulletColor.withValues(alpha: 0.2) : Colors.grey[850],
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: isTournament ? _bulletColor : Colors.grey[700]!, color: isTournament ? _bulletColor : Colors.grey[700]!,
@@ -376,14 +360,25 @@ Future<void> _renameFolder(String oldFolderName) async {
), ),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Center( child: Center(
child: Text( child: Row(
isTournament ? _t('Tournament Mode 🏆') : _t('Mark as Tournament 🏆'), mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.emoji_events,
size: 16,
color: isTournament ? _bulletColor : Colors.white70,
),
const SizedBox(width: 8),
Text(
isTournament ? _t('Tournament Mode') : _t('Mark as Tournament'),
style: TextStyle( style: TextStyle(
color: isTournament ? _bulletColor : Colors.white70, color: isTournament ? _bulletColor : Colors.white70,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 16, fontSize: 16,
), ),
), ),
],
),
), ),
), ),
), ),
@@ -445,9 +440,7 @@ Future<void> _renameFolder(String oldFolderName) async {
if (targetFolderName != oldFolderName) { if (targetFolderName != oldFolderName) {
if (await newFolderPath.exists()) { if (await newFolderPath.exists()) {
ScaffoldMessenger.of(context).showSnackBar( debugPrint('A folder named "$targetFolderName" already exists.');
SnackBar(content: Text('A folder named "$targetFolderName" already exists.')),
);
return; return;
} else { } else {
await oldFolderPath.rename(newFolderPath.path); await oldFolderPath.rename(newFolderPath.path);
@@ -498,16 +491,8 @@ Future<void> _renameFolder(String oldFolderName) async {
} }
_loadTargetDataFiles(); // Refresh file/folder list _loadTargetDataFiles(); // Refresh file/folder list
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Folder "$folderName" deleted successfully.'),
),
);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( debugPrint("Error deleting folder: $e");
SnackBar(content: Text("Error deleting folder: $e")),
);
} }
} }
} }
@@ -594,14 +579,14 @@ Widget _buildFolderRow(String folderName) {
: 'Unknown Date'; : 'Unknown Date';
return Container( return Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: Colors.grey[300],
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
children: [ children: [
// Header section: Name and Date // Header section with grey background like toolbars
InkWell( InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -613,40 +598,56 @@ Widget _buildFolderRow(String folderName) {
topRight: Radius.circular(12), topRight: Radius.circular(12),
), ),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
children: [ children: [
// Small colored indicator // White indicator line
Container( Container(
width: 4, width: 4,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _bulletColor, color: Colors.white,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 12),
Row(
children: [
if (_folderTypes[folderName] == true) ...[
const Icon(Icons.emoji_events, size: 18, color: Colors.white),
const SizedBox(width: 8), const SizedBox(width: 8),
],
Text( Text(
_folderTypes[folderName] == true folderName,
? '🏆 $folderName' style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
: folderName, ),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _bulletColor), ],
), ),
], ],
), ),
Row( Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.calendar_today, size: 14, color: Colors.white),
const SizedBox(width: 6),
Text( Text(
"📅 $formattedDate", formattedDate,
style: const TextStyle(fontSize: 13, color: Colors.black87), style: const TextStyle(fontSize: 13, color: Colors.white),
), ),
const SizedBox(width: 8), const SizedBox(width: 12),
Icon( Icon(
_expandedFolder == folderName ? Icons.expand_less : Icons.expand_more, _expandedFolder == folderName ? Icons.expand_less : Icons.expand_more,
color: Colors.black87, color: Colors.white,
), ),
], ],
), ),
@@ -698,7 +699,7 @@ Widget _buildFolderRow(String folderName) {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.emoji_events, size: 16, color: Colors.amber), const Icon(Icons.emoji_events, size: 16, color: Colors.black87),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
"$totalScore", "$totalScore",
@@ -816,7 +817,7 @@ Widget _buildTargetsList(String folderName) {
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon( const Icon(
Icons.emoji_events, Icons.emoji_events,
color: Colors.amber, color: Colors.black87,
size: 14, size: 14,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@@ -868,6 +869,7 @@ Widget _buildTargetsList(String folderName) {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: _backgroundColor,
body: Column( body: Column(
children: [ children: [
// 🔽 Filter dropdown at the top (styled like tab bar) // 🔽 Filter dropdown at the top (styled like tab bar)
@@ -893,14 +895,29 @@ Widget build(BuildContext context) {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_folderFilter == 'all'
? Icons.folder
: _folderFilter == 'tournament'
? Icons.emoji_events
: Icons.track_changes,
size: 16,
color: Colors.black,
),
const SizedBox(width: 6),
Text( Text(
_folderFilter == 'all' _folderFilter == 'all'
? _t('Showing: 📂 All folders') ? _t('Showing: All folders')
: _folderFilter == 'tournament' : _folderFilter == 'tournament'
? _t('Showing: 🏆 Tournament folders') ? _t('Showing: Tournament folders')
: _t('Showing: 🎯 Training folders'), : _t('Showing: Training folders'),
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black),
), ),
],
),
Icon(Icons.filter_list, color: Colors.black87, size: 18), Icon(Icons.filter_list, color: Colors.black87, size: 18),
], ],
), ),
@@ -917,6 +934,7 @@ Widget build(BuildContext context) {
thickness: 6, // optional: adjust width thickness: 6, // optional: adjust width
radius: const Radius.circular(8), radius: const Radius.circular(8),
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: _filteredFolderNames().length, itemCount: _filteredFolderNames().length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
String folderName = _filteredFolderNames()[index]; String folderName = _filteredFolderNames()[index];
@@ -955,11 +973,11 @@ void _showFolderFilterPopup(BuildContext context) {
const SizedBox(height: 24), const SizedBox(height: 24),
// Folder type filters // Folder type filters
_buildStyledFilterButton('all', _t('📂 All folders'), context), _buildStyledFilterButton('all', Icons.folder, _t('All folders'), context),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStyledFilterButton('tournament', _t('🏆 Tournament'), context), _buildStyledFilterButton('tournament', Icons.emoji_events, _t('Tournament'), context),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStyledFilterButton('training', _t('🎯 Training'), context), _buildStyledFilterButton('training', Icons.track_changes, _t('Training'), context),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -986,8 +1004,17 @@ void _showFolderFilterPopup(BuildContext context) {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: Center(
child: Text( child: Row(
_t('📅 Sort by Date'), mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_today,
size: 14,
color: _sortMode == 'date' ? Colors.white : Colors.white70,
),
const SizedBox(width: 4),
Text(
_t('Sort by Date'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: _sortMode == 'date' ? Colors.white : Colors.white70, color: _sortMode == 'date' ? Colors.white : Colors.white70,
@@ -997,6 +1024,8 @@ void _showFolderFilterPopup(BuildContext context) {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
],
),
), ),
), ),
), ),
@@ -1015,8 +1044,17 @@ void _showFolderFilterPopup(BuildContext context) {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: Center(
child: Text( child: Row(
_t('🏆 Sort by Score'), mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.emoji_events,
size: 14,
color: _sortMode == 'score' ? Colors.white : Colors.white70,
),
const SizedBox(width: 4),
Text(
_t('Sort by Score'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: _sortMode == 'score' ? Colors.white : Colors.white70, color: _sortMode == 'score' ? Colors.white : Colors.white70,
@@ -1026,6 +1064,8 @@ void _showFolderFilterPopup(BuildContext context) {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
],
),
), ),
), ),
), ),
@@ -1043,7 +1083,7 @@ void _showFolderFilterPopup(BuildContext context) {
} }
Widget _buildStyledFilterButton(String value, String label, BuildContext context) { Widget _buildStyledFilterButton(String value, IconData icon, String label, BuildContext context) {
final isSelected = _folderFilter == value; final isSelected = _folderFilter == value;
return SizedBox( return SizedBox(
@@ -1062,7 +1102,16 @@ Widget _buildStyledFilterButton(String value, String label, BuildContext context
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: Center(
child: Text( child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 18,
color: isSelected ? Colors.white : Colors.white70,
),
const SizedBox(width: 8),
Text(
label, label,
style: TextStyle( style: TextStyle(
color: isSelected ? Colors.white : Colors.white70, color: isSelected ? Colors.white : Colors.white70,
@@ -1070,6 +1119,8 @@ Widget _buildStyledFilterButton(String value, String label, BuildContext context
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
],
),
), ),
), ),
), ),
+213 -50
View File
@@ -5,7 +5,9 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../services/google_calendar_service.dart'; import '../services/google_calendar_service.dart';
import '../services/notification_service.dart';
import '../utils/translations.dart'; import '../utils/translations.dart';
import '../utils/target_colors.dart';
class Contact { class Contact {
final String firstName; final String firstName;
@@ -45,6 +47,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
String _selectedLanguage = 'English'; String _selectedLanguage = 'English';
bool _showContacts = false; bool _showContacts = false;
bool _showDebugPreview = false; bool _showDebugPreview = false;
late Color _backgroundColor;
// Members contact list // Members contact list
final List<Contact> _contacts = [ final List<Contact> _contacts = [
@@ -69,8 +72,11 @@ class _CalendarScreenState extends State<CalendarScreen> {
void initState() { void initState() {
super.initState(); super.initState();
initializeDateFormatting('sl_SI', null); // Initialize Slovenian locale initializeDateFormatting('sl_SI', null); // Initialize Slovenian locale
// Initialize with default target background color
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
_loadBulletColor(); _loadBulletColor();
_loadLanguage(); _loadLanguage();
_loadSelectedTargetColor();
_loadEvents(); _loadEvents();
} }
@@ -85,6 +91,14 @@ class _CalendarScreenState extends State<CalendarScreen> {
}); });
} }
Future<void> _loadSelectedTargetColor() async {
final prefs = await SharedPreferences.getInstance();
final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png';
setState(() {
_backgroundColor = TargetColors.getBackgroundColor(selectedTarget);
});
}
void _toggleDebugPreview() { void _toggleDebugPreview() {
setState(() { setState(() {
_showDebugPreview = !_showDebugPreview; _showDebugPreview = !_showDebugPreview;
@@ -221,6 +235,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
_events = filteredEvents; _events = filteredEvents;
}); });
} }
NotificationService().scheduleEventNotifications(filteredEvents);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -473,15 +488,15 @@ class _CalendarScreenState extends State<CalendarScreen> {
onTap: () => _makePhoneCall(contact.phone), onTap: () => _makePhoneCall(contact.phone),
onLongPress: () => _showContactPopup(context, contact), onLongPress: () => _showContactPopup(context, contact),
child: Container( child: Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: Colors.grey[300],
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.withValues(alpha: 0.08), color: Colors.grey.withValues(alpha: 0.3),
blurRadius: 2, blurRadius: 8,
offset: const Offset(0, 1), offset: const Offset(0, 2),
), ),
], ],
), ),
@@ -490,37 +505,37 @@ class _CalendarScreenState extends State<CalendarScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Colored header bar (like event cards) // Header bar with bullet color and transparency
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.2), color: accentColor.withValues(alpha: 0.6),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10), topLeft: Radius.circular(12),
topRight: Radius.circular(10), topRight: Radius.circular(12),
), ),
), ),
child: Row( child: Row(
children: [ children: [
// Small colored indicator // White indicator line
Container( Container(
width: 3, width: 4,
height: 12, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor, color: Colors.white,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 7), const SizedBox(width: 12),
// Contact name // Contact name in white
Expanded( Expanded(
child: Text( child: Text(
_capitalize(contact.fullName), _capitalize(contact.fullName),
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: accentColor, color: Colors.white,
), ),
), ),
), ),
@@ -670,6 +685,22 @@ class _CalendarScreenState extends State<CalendarScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildSampleEventCard(accentColor, EventType.sestanek), _buildSampleEventCard(accentColor, EventType.sestanek),
const SizedBox(height: 32),
// Other dates section
Text(
'Other Dates (without emphasis)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
_buildOtherDateEventCard(accentColor, EventType.regular, 'Tomorrow'),
const SizedBox(height: 12),
_buildOtherDateEventCard(accentColor, EventType.competition, 'Next Week'),
const SizedBox(height: 12),
_buildOtherDateEventCard(accentColor, EventType.sestanek, 'In 5 days'),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
), ),
@@ -717,7 +748,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
} }
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: _backgroundColor,
body: Column( body: Column(
children: [ children: [
// Tab buttons // Tab buttons
@@ -928,7 +959,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
time = '18:00 - 19:00'; time = '18:00 - 19:00';
break; break;
case EventType.regular: case EventType.regular:
cardAccentColor = Colors.blue; cardAccentColor = accentColor;
eventIcon = Icons.event; eventIcon = Icons.event;
title = _t('Duty Shift'); title = _t('Duty Shift');
time = '14:00 - 16:00'; time = '14:00 - 16:00';
@@ -942,11 +973,11 @@ class _CalendarScreenState extends State<CalendarScreen> {
color: backgroundColor, color: backgroundColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
// Card glow effect for today's events // Strong glow effect for today's events (sample is always today)
BoxShadow( BoxShadow(
color: cardAccentColor.withValues(alpha: 0.3), color: cardAccentColor.withValues(alpha: 0.4),
blurRadius: 16, blurRadius: 20,
offset: const Offset(0, 4), offset: const Offset(0, 6),
), ),
// Subtle inner shadow // Subtle inner shadow
BoxShadow( BoxShadow(
@@ -964,7 +995,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardAccentColor.withValues(alpha: 0.2), color: cardAccentColor,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
topRight: Radius.circular(12), topRight: Radius.circular(12),
@@ -972,26 +1003,167 @@ class _CalendarScreenState extends State<CalendarScreen> {
), ),
child: Row( child: Row(
children: [ children: [
// Small colored indicator // Small white indicator line
Container( Container(
width: 4, width: 4,
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardAccentColor, color: Colors.white,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
_t('Today'), _t('Today'),
style: TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: cardAccentColor, color: Colors.white,
), ),
), ),
const Spacer(), const Spacer(),
// Event type badge - filled badges for all types // Event type badge - filled badges for all types
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: cardAccentColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(eventIcon, size: 12, color: Colors.white),
const SizedBox(width: 4),
Text(
eventType == EventType.competition
? _t('TEKMOWANIE')
: eventType == EventType.sestanek
? _t('SESTANEK')
: _t('DEŽURSTVO'),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 0.8,
),
),
],
),
),
],
),
),
// Main content
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title with icon
Row(
children: [
Icon(eventIcon, size: 20, color: cardAccentColor),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
],
),
const SizedBox(height: 8),
// Time range
_buildDetailRowInline(
icon: Icons.access_time,
value: time,
accentColor: cardAccentColor,
),
],
),
),
],
),
),
);
}
Widget _buildOtherDateEventCard(Color accentColor, EventType eventType, String dateLabel) {
// Color coding for each event type
Color cardAccentColor;
Color backgroundColor = Colors.grey[300]!;
IconData eventIcon;
String title;
String time;
switch (eventType) {
case EventType.competition:
cardAccentColor = Colors.orange;
eventIcon = Icons.emoji_events;
title = _t('League / Competition');
time = '09:00 - 18:00';
break;
case EventType.sestanek:
cardAccentColor = Colors.red;
eventIcon = Icons.groups;
title = _t('Team Meeting');
time = '18:00 - 19:00';
break;
case EventType.regular:
cardAccentColor = accentColor;
eventIcon = Icons.event;
title = _t('Duty Shift');
time = '14:00 - 16:00';
break;
}
return RepaintBoundary(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
decoration: BoxDecoration(
color: backgroundColor,
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: cardAccentColor.withValues(alpha: 0.7),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
// Small white indicator line
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
dateLabel,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const Spacer(),
// Event type badge
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -1052,13 +1224,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
value: time, value: time,
accentColor: cardAccentColor, accentColor: cardAccentColor,
), ),
const SizedBox(height: 6),
// Location
_buildDetailRowInline(
icon: Icons.location_on,
value: _t('Strelišče Ljubljana'),
accentColor: cardAccentColor,
),
], ],
), ),
), ),
@@ -1090,7 +1255,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
eventIcon = Icons.groups; // People icon for meetings eventIcon = Icons.groups; // People icon for meetings
break; break;
case EventType.regular: case EventType.regular:
cardAccentColor = Colors.blue; cardAccentColor = accentColor;
backgroundColor = Colors.grey[300]!; backgroundColor = Colors.grey[300]!;
eventIcon = Icons.event; eventIcon = Icons.event;
break; break;
@@ -1131,20 +1296,20 @@ class _CalendarScreenState extends State<CalendarScreen> {
color: backgroundColor, color: backgroundColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
// Card glow effect (stronger for today's events) // Strong glow effect only for today's events
if (isToday) if (isToday) ...[
BoxShadow( BoxShadow(
color: cardAccentColor.withValues(alpha: 0.3), color: cardAccentColor.withValues(alpha: 0.4),
blurRadius: 16, blurRadius: 20,
offset: const Offset(0, 4), offset: const Offset(0, 6),
), ),
// Subtle inner shadow
BoxShadow( BoxShadow(
color: Colors.grey.withValues(alpha: 0.1), color: Colors.grey.withValues(alpha: 0.1),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
], ],
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1154,9 +1319,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isToday color: isToday ? cardAccentColor : cardAccentColor.withValues(alpha: 0.7),
? cardAccentColor.withValues(alpha: 0.2)
: cardAccentColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
topRight: Radius.circular(12), topRight: Radius.circular(12),
@@ -1164,22 +1327,22 @@ class _CalendarScreenState extends State<CalendarScreen> {
), ),
child: Row( child: Row(
children: [ children: [
// Small colored indicator // Small white indicator line
Container( Container(
width: 4, width: 4,
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardAccentColor, color: Colors.white,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
dateText, dateText,
style: TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: cardAccentColor, color: Colors.white,
), ),
), ),
const Spacer(), const Spacer(),
+14
View File
@@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'yolo_detector.dart'; import 'yolo_detector.dart';
import 'preview_screen.dart'; import 'preview_screen.dart';
import '../utils/translations.dart'; import '../utils/translations.dart';
import '../utils/target_colors.dart';
class CameraScreen extends StatefulWidget { class CameraScreen extends StatefulWidget {
const CameraScreen({super.key}); const CameraScreen({super.key});
@@ -23,6 +24,7 @@ class _CameraScreenState extends State<CameraScreen> {
// The variable to hold the number of bullets entered by the user (default set to 5) // The variable to hold the number of bullets entered by the user (default set to 5)
int _numberOfBullets = 5; int _numberOfBullets = 5;
String _selectedLanguage = 'English'; String _selectedLanguage = 'English';
late Color _backgroundColor;
// Brightness and contrast controls // Brightness and contrast controls
double _brightness = 0.0; // -1.0 to 1.0 double _brightness = 0.0; // -1.0 to 1.0
@@ -31,10 +33,13 @@ class _CameraScreenState extends State<CameraScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize with default target background color
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
_loadBulletCount(); _loadBulletCount();
_initCamera(); _initCamera();
_loadBulletColor(); // 👈 _loadBulletColor(); // 👈
_loadLanguage(); _loadLanguage();
_loadSelectedTargetColor();
_loadBrightnessContrast(); _loadBrightnessContrast();
YOLODetector.create().then((detector) => _yoloDetector = detector); YOLODetector.create().then((detector) => _yoloDetector = detector);
} }
@@ -64,6 +69,14 @@ class _CameraScreenState extends State<CameraScreen> {
}); });
} }
Future<void> _loadSelectedTargetColor() async {
final prefs = await SharedPreferences.getInstance();
final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png';
setState(() {
_backgroundColor = TargetColors.getBackgroundColor(selectedTarget);
});
}
Future<void> _loadBulletColor() async { Future<void> _loadBulletColor() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final selected = prefs.getString('bulletColor') ?? 'Blue'; final selected = prefs.getString('bulletColor') ?? 'Blue';
@@ -445,6 +458,7 @@ Color _getColorFromName(String colorName) {
} }
return Scaffold( return Scaffold(
backgroundColor: _backgroundColor,
extendBody: true, extendBody: true,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.grey[900], backgroundColor: Colors.grey[900],
+137
View File
@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import '../utils/translations.dart';
class CameraSelectionDialog extends StatefulWidget {
final List<Map<String, dynamic>> availableCameras;
final String selectedLanguage;
const CameraSelectionDialog({
super.key,
required this.availableCameras,
required this.selectedLanguage,
});
@override
State<CameraSelectionDialog> createState() => _CameraSelectionDialogState();
}
class _CameraSelectionDialogState extends State<CameraSelectionDialog> {
late List<bool> _selectedCameras;
@override
void initState() {
super.initState();
_selectedCameras = List.generate(widget.availableCameras.length, (_) => false);
}
String _t(String key) {
return AppTranslations.translate(key, widget.selectedLanguage);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: Colors.grey[900],
title: Text(
_t('Select Cameras to Spectate'),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_t('Choose 1-6 cameras'),
style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
),
),
const SizedBox(height: 16),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.availableCameras.length,
itemBuilder: (context, index) {
final camera = widget.availableCameras[index];
final cameraIndex = camera['index'] as int;
final cameraIp = camera['ip'] as String;
return CheckboxListTile(
title: Text(
"${_t('Camera')} $cameraIndex",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
cameraIp,
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
),
value: _selectedCameras[index],
onChanged: (value) {
setState(() {
_selectedCameras[index] = value ?? false;
});
},
activeColor: Colors.grey[800],
checkColor: Colors.white,
tileColor: Colors.grey[850],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
_t('Cancel'),
style: const TextStyle(color: Colors.grey),
),
),
TextButton(
onPressed: _selectedCameras.contains(true)
? () {
final selectedCameras = <Map<String, dynamic>>[];
for (int i = 0; i < _selectedCameras.length; i++) {
if (_selectedCameras[i]) {
selectedCameras.add(widget.availableCameras[i]);
}
}
Navigator.pop(context, selectedCameras);
}
: null,
child: Text(
_t('View Cameras'),
style: TextStyle(
color: _selectedCameras.contains(true)
? Colors.white
: Colors.grey[600],
fontWeight: FontWeight.w600,
),
),
),
],
);
}
}
+33 -3
View File
@@ -6,9 +6,10 @@ import 'target_screen.dart';
import '../widgets/bottom_nav.dart'; import '../widgets/bottom_nav.dart';
//import 'login_screen.dart'; //import 'login_screen.dart';
import 'camera_screen.dart'; import 'camera_screen.dart';
import 'spectator_screen.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../utils/translations.dart'; import '../utils/translations.dart';
import '../utils/target_colors.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@@ -20,6 +21,8 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 1; // Default to Shooting Screen int _selectedIndex = 1; // Default to Shooting Screen
String _selectedLanguage = 'English'; String _selectedLanguage = 'English';
String _selectedTarget = 'assets/target.png';
late Color _backgroundColor;
static final List<Widget> _pages = <Widget>[ static final List<Widget> _pages = <Widget>[
BookScreen(), BookScreen(),
@@ -40,7 +43,10 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize with default target background color
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
_loadLanguage(); _loadLanguage();
_loadSelectedTarget();
} }
String _t(String key) { String _t(String key) {
@@ -54,6 +60,16 @@ class _HomeScreenState extends State<HomeScreen> {
}); });
} }
Future<void> _loadSelectedTarget() async {
final prefs = await SharedPreferences.getInstance();
final selectedTarget =
prefs.getString('selectedTarget') ?? 'assets/target.png';
setState(() {
_selectedTarget = selectedTarget;
_backgroundColor = TargetColors.getBackgroundColor(selectedTarget);
});
}
void _onItemTapped(int index) { void _onItemTapped(int index) {
if (index == 2) { if (index == 2) {
_openCameraScan(); _openCameraScan();
@@ -99,11 +115,25 @@ class _HomeScreenState extends State<HomeScreen> {
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
scrolledUnderElevation: 0, // For Material 3 (Flutter 3.7+) scrolledUnderElevation: 0, // For Material 3 (Flutter 3.7+)
actions: [ actions: [
if (_selectedIndex == 1)
IconButton(
icon: const Icon(Icons.visibility_rounded, color: Colors.white),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SpectatorScreen(
selectedLanguage: _selectedLanguage,
),
),
);
},
),
Padding( Padding(
padding: const EdgeInsets.only(right: 16.0), padding: const EdgeInsets.only(right: 16.0),
child: Center( child: Center(
child: Text( child: Text(
'V1.0.2', 'V1.1',
style: TextStyle( style: TextStyle(
color: Colors.grey[400], color: Colors.grey[400],
fontSize: 12, fontSize: 12,
@@ -114,6 +144,7 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
], ],
), ),
backgroundColor: _backgroundColor,
body: _pages[_selectedIndex], body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavBar( bottomNavigationBar: BottomNavBar(
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex,
@@ -123,4 +154,3 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
} }
+248
View File
@@ -0,0 +1,248 @@
import 'package:flutter/material.dart';
import 'package:flutter_mjpeg/flutter_mjpeg.dart';
import '../utils/translations.dart';
class MultiCameraScreen extends StatefulWidget {
final List<Map<String, dynamic>> cameras;
final String selectedLanguage;
const MultiCameraScreen({
super.key,
required this.cameras,
required this.selectedLanguage,
});
@override
State<MultiCameraScreen> createState() => _MultiCameraScreenState();
}
class _MultiCameraScreenState extends State<MultiCameraScreen> {
int? _fullscreenIndex; // null = grid view, index = fullscreen camera
String _t(String key) {
return AppTranslations.translate(key, widget.selectedLanguage);
}
// Calculate grid columns based on camera count
int get _crossAxisCount {
final count = widget.cameras.length;
if (count == 1) return 1;
if (count <= 4) return 2;
return 3; // 5-6 cameras
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
_fullscreenIndex == null
? _t('Multi-Camera View')
: "${_t('Camera')} ${widget.cameras[_fullscreenIndex!]['index']}",
),
backgroundColor: Colors.grey[900],
foregroundColor: Colors.white,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
if (_fullscreenIndex != null) {
setState(() {
_fullscreenIndex = null; // Exit fullscreen first
});
} else {
Navigator.pop(context); // Exit screen
}
},
),
),
backgroundColor: Colors.black,
body: _fullscreenIndex == null
? _buildGridView()
: _buildFullscreenView(_fullscreenIndex!),
);
}
Widget _buildGridView() {
return GridView.builder(
padding: const EdgeInsets.all(8),
itemCount: widget.cameras.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _crossAxisCount,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 4 / 3, // 4:3 aspect ratio for camera feeds
),
itemBuilder: (context, index) {
return _buildCameraCell(index);
},
);
}
Widget _buildCameraCell(int index) {
final camera = widget.cameras[index];
final cameraIndex = camera['index'] as int;
final cameraIp = camera['ip'] as String;
final streamUrl = cameraIp.startsWith(RegExp(r"https?://"))
? cameraIp
: "http://$cameraIp";
return GestureDetector(
onTap: () {
setState(() {
_fullscreenIndex = index; // Enter fullscreen
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[700]!, width: 1),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
// MJPEG Stream
Mjpeg(
stream: streamUrl,
isLive: true,
fit: BoxFit.contain,
error: (context, error, stackTrace) {
return _buildErrorWidget(cameraIndex);
},
),
// Camera label overlay
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(4),
),
child: Text(
"${_t('Camera')} $cameraIndex",
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
);
}
Widget _buildFullscreenView(int index) {
final camera = widget.cameras[index];
final cameraIndex = camera['index'] as int;
final cameraIp = camera['ip'] as String;
final streamUrl = cameraIp.startsWith(RegExp(r"https?://"))
? cameraIp
: "http://$cameraIp";
return GestureDetector(
onTap: () {
setState(() {
_fullscreenIndex = null; // Exit fullscreen, return to grid
});
},
child: Container(
color: Colors.black,
child: Stack(
children: [
Center(
child: Mjpeg(
stream: streamUrl,
isLive: true,
fit: BoxFit.contain,
error: (context, error, stackTrace) {
return _buildErrorWidget(cameraIndex);
},
),
),
// Back indicator overlay
Positioned(
bottom: 16,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.touch_app, color: Colors.white, size: 16),
const SizedBox(width: 8),
Text(
_t('Tap to return to grid'),
style:
const TextStyle(color: Colors.white, fontSize: 14),
),
],
),
),
),
),
// Camera label
Positioned(
top: 16,
left: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(6),
),
child: Text(
"${_t('Camera')} $cameraIndex",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
}
Widget _buildErrorWidget(int cameraIndex) {
return Container(
color: Colors.black,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.videocam_off, color: Colors.red, size: 32),
const SizedBox(height: 4),
Flexible(
child: Text(
"${_t('Camera')} $cameraIndex\n${_t('Stream unavailable')}",
style: const TextStyle(
color: Colors.white70,
fontSize: 11,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+401
View File
@@ -0,0 +1,401 @@
import 'package:flutter/material.dart';
import 'package:flutter_mjpeg/flutter_mjpeg.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/translations.dart';
class SpectatorScreen extends StatefulWidget {
final String selectedLanguage;
const SpectatorScreen({
super.key,
required this.selectedLanguage,
});
@override
State<SpectatorScreen> createState() => _SpectatorScreenState();
}
class _SpectatorScreenState extends State<SpectatorScreen> {
List<bool> _selectedCameras = List.generate(6, (_) => true); // All selected by default
List<bool> _enabledCameras = List.generate(6, (_) => false); // Which cameras have IPs configured
List<Map<String, dynamic>> _cameras = [];
Color _bulletColor = Colors.blue; // default/fallback bullet color
@override
void initState() {
super.initState();
_loadCameras();
_loadBulletColor();
}
String _t(String key) {
return AppTranslations.translate(key, widget.selectedLanguage);
}
Future<void> _loadCameras() async {
final prefs = await SharedPreferences.getInstance();
List<Map<String, dynamic>> cameras = [];
List<bool> enabledCameras = List.generate(6, (_) => false);
for (int i = 0; i < 6; i++) {
String? ip = prefs.getString('cameraIP_${i + 1}');
if (ip != null && ip.isNotEmpty && ip != 'Not Set') {
cameras.add({
'index': i + 1,
'ip': ip,
});
enabledCameras[i] = true; // Mark camera as enabled
}
}
if (mounted) {
setState(() {
_cameras = cameras;
_enabledCameras = enabledCameras;
});
}
}
Future<void> _loadBulletColor() async {
final prefs = await SharedPreferences.getInstance();
final selected = prefs.getString('bulletColor') ?? 'Blue';
setState(() {
_bulletColor = _getColorFromName(selected);
});
}
Color _getColorFromName(String colorName) {
switch (colorName.toLowerCase()) {
case 'blue':
return Colors.blue;
case 'green':
return Colors.green;
case 'yellow':
return Colors.yellow;
case 'purple':
return Colors.purple;
case 'cyan':
return Colors.cyan;
case 'teal':
return Colors.teal;
case 'indigo':
return Colors.indigo;
case 'pink':
return Colors.pink;
case 'lime':
return Colors.lime;
case 'mint':
return Colors.greenAccent;
case 'orange':
return Colors.orange;
case 'violet':
return Colors.deepPurpleAccent;
case 'amber':
return Colors.amber;
case 'brown':
return Colors.brown;
case 'grey':
return Colors.grey;
default:
return Colors.blue;
}
}
void _showCameraSelection() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setModalState) => Dialog(
backgroundColor: Colors.grey[900],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_t('Select Cameras'),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 24),
// Row 1: Cameras 1-3
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(3, (index) {
final cameraNum = index + 1;
final isSelected = _selectedCameras[index];
final isEnabled = _enabledCameras[index];
return GestureDetector(
onTap: isEnabled
? () {
// Update both modal and parent state simultaneously
_selectedCameras[index] = !_selectedCameras[index];
setModalState(() {});
if (mounted) {
setState(() {});
}
}
: null,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: !isEnabled
? Colors.grey[700]
: (isSelected ? _bulletColor : Colors.grey[800]),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: !isEnabled
? Colors.grey[600]!
: (isSelected ? _bulletColor : Colors.grey[700]!),
width: 2,
),
),
child: Center(
child: Text(
'$cameraNum',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
);
}),
),
const SizedBox(height: 16),
// Row 2: Cameras 4-6
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(3, (index) {
final cameraNum = index + 4;
final arrayIndex = index + 3;
final isSelected = _selectedCameras[arrayIndex];
final isEnabled = _enabledCameras[arrayIndex];
return GestureDetector(
onTap: isEnabled
? () {
// Update both modal and parent state simultaneously
_selectedCameras[arrayIndex] = !_selectedCameras[arrayIndex];
setModalState(() {});
if (mounted) {
setState(() {});
}
}
: null,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: !isEnabled
? Colors.grey[700]
: (isSelected ? _bulletColor : Colors.grey[800]),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: !isEnabled
? Colors.grey[600]!
: (isSelected ? _bulletColor : Colors.grey[700]!),
width: 2,
),
),
child: Center(
child: Text(
'$cameraNum',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
);
}),
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey[700]!, width: 1),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 40,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _bulletColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () => Navigator.pop(context),
child: Text(
_t('Done'),
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
final visibleCameras = <Map<String, dynamic>>[];
for (int i = 0; i < _cameras.length; i++) {
if (_selectedCameras[_cameras[i]['index'] - 1]) {
visibleCameras.add(_cameras[i]);
}
}
// Portrait list view
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: Text(
_t('Spectate'),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.grey[900],
elevation: 0,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: _showCameraSelection,
),
],
),
body: visibleCameras.isEmpty
? Center(
child: Text(
_t('No cameras selected'),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 16),
itemCount: visibleCameras.length,
itemBuilder: (context, index) {
final camera = visibleCameras[index];
final cameraIndex = camera['index'] as int;
final cameraIp = camera['ip'] as String;
final streamUrl = cameraIp.startsWith(RegExp(r"https?://"))
? cameraIp
: "http://$cameraIp";
// 4:3 aspect ratio = width / height (640x480 stream)
return Container(
margin: const EdgeInsets.only(bottom: 4),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[800]!, width: 1),
),
child: AspectRatio(
aspectRatio: 4 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
Mjpeg(
stream: streamUrl,
isLive: true,
fit: BoxFit.cover,
error: (context, error, stackTrace) => Container(
color: Colors.grey[700],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.videocam_off,
color: Colors.white,
size: 32,
),
const SizedBox(height: 8),
Text(
"${_t('Camera')} $cameraIndex",
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
// Camera number circle - top right (with bullet color)
Positioned(
top: 8,
right: 8,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: _bulletColor,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$cameraIndex',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
),
);
},
),
);
}
}
+104 -10
View File
@@ -748,11 +748,22 @@ List<double> _multiplyMatrices(List<double> a, List<double> b) {
child: ColorFiltered( child: ColorFiltered(
colorFilter: _buildColorFilter(), colorFilter: _buildColorFilter(),
child: Mjpeg( child: Mjpeg(
key: ValueKey(_streamUrl),
stream: _streamUrl!, stream: _streamUrl!,
isLive: true, isLive: true,
fit: BoxFit.contain, fit: BoxFit.contain,
error: (context, error, stackTrace) { error: (context, error, stackTrace) {
return _buildErrorContent(); // Mark as error so we can display error placeholder outside filters
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isError = true;
});
}
});
return SizedBox.expand(
child: Container(color: Colors.black),
);
}, },
), ),
), ),
@@ -845,21 +856,104 @@ List<double> _multiplyMatrices(List<double> a, List<double> b) {
} }
Widget _buildErrorContent() { Widget _buildErrorContent() {
return Column( return Center(
mainAxisAlignment: MainAxisAlignment.center, child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.error, color: Colors.red, size: 100), Icon(Icons.error, color: _bulletColor, size: 100),
const SizedBox(height: 20), const SizedBox(height: 24),
Text( Text(
_t("Unable to load camera stream"), _t("Unable to load camera stream"),
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_t("Please ensure:"),
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.wifi, color: Colors.white, size: 18),
const SizedBox(width: 12),
Expanded(
child: Text(
_t("WiFi is enabled"),
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
), ),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _loadStreamUrl,
child: Text(_t("Retry")),
), ),
], ],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.signal_cellular_off, color: Colors.white, size: 18),
const SizedBox(width: 12),
Expanded(
child: Text(
_t("Mobile data is disabled"),
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
),
],
),
],
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
onPressed: _loadStreamUrl,
icon: const Icon(Icons.refresh, size: 20),
label: Text(_t("Reload")),
style: ElevatedButton.styleFrom(
backgroundColor: _bulletColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
),
),
),
); );
} }
} }
+24 -6
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.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'; import '../utils/translations.dart';
import '../utils/target_colors.dart';
class TargetScreen extends StatefulWidget { class TargetScreen extends StatefulWidget {
const TargetScreen({super.key}); const TargetScreen({super.key});
@@ -14,11 +15,15 @@ class _TargetScreenState extends State<TargetScreen> {
List<bool> _cameraButtonsEnabled = List<bool> _cameraButtonsEnabled =
List.generate(6, (_) => false); // Initially, all buttons are disabled List.generate(6, (_) => false); // Initially, all buttons are disabled
String _selectedLanguage = 'English'; String _selectedLanguage = 'English';
late Color _backgroundColor;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize with default target background color
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
_loadLanguage(); _loadLanguage();
_loadSelectedTargetColor();
_loadCameraData(); _loadCameraData();
} }
@@ -33,6 +38,14 @@ class _TargetScreenState extends State<TargetScreen> {
}); });
} }
Future<void> _loadSelectedTargetColor() async {
final prefs = await SharedPreferences.getInstance();
final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png';
setState(() {
_backgroundColor = TargetColors.getBackgroundColor(selectedTarget);
});
}
/// Load the camera IPs from SharedPreferences and update button status /// Load the camera IPs from SharedPreferences and update button status
Future<void> _loadCameraData() async { Future<void> _loadCameraData() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@@ -63,14 +76,18 @@ class _TargetScreenState extends State<TargetScreen> {
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: _backgroundColor,
body: Padding( body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column( child: Column(
children: [ children: [
Expanded(
child: Column(
children: [
// Camera grid (6 cameras)
Expanded( Expanded(
child: GridView.builder( child: GridView.builder(
itemCount: 6, itemCount: 6,
@@ -78,11 +95,11 @@ class _TargetScreenState extends State<TargetScreen> {
crossAxisCount: 2, crossAxisCount: 2,
mainAxisSpacing: 16, mainAxisSpacing: 16,
crossAxisSpacing: 16, crossAxisSpacing: 16,
childAspectRatio: 1, // square-ish buttons childAspectRatio: 1,
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isEnabled = _cameraButtonsEnabled[index]; final isEnabled = _cameraButtonsEnabled[index];
return InkWell( return GestureDetector(
onTap: isEnabled onTap: isEnabled
? () { ? () {
Navigator.push( Navigator.push(
@@ -94,7 +111,6 @@ class _TargetScreenState extends State<TargetScreen> {
); );
} }
: null, : null,
borderRadius: BorderRadius.circular(16),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isEnabled ? Colors.grey[900] : Colors.grey[300], color: isEnabled ? Colors.grey[900] : Colors.grey[300],
@@ -114,8 +130,7 @@ class _TargetScreenState extends State<TargetScreen> {
"${_t('Camera')} ${index + 1}", "${_t('Camera')} ${index + 1}",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: color: isEnabled ? Colors.white : Colors.grey[600],
isEnabled ? Colors.white : Colors.grey[600],
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@@ -130,6 +145,9 @@ class _TargetScreenState extends State<TargetScreen> {
], ],
), ),
), ),
],
),
),
); );
} }
} }
+564 -66
View File
@@ -6,6 +6,7 @@ import 'dart:convert';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../utils/translations.dart'; import '../utils/translations.dart';
import '../utils/target_colors.dart';
class TargetViewScreen extends StatefulWidget { class TargetViewScreen extends StatefulWidget {
final List<File> jsonFiles; final List<File> jsonFiles;
@@ -26,6 +27,7 @@ class TargetViewScreen extends StatefulWidget {
class _TargetViewScreenState extends State<TargetViewScreen> { class _TargetViewScreenState extends State<TargetViewScreen> {
int _currentIndex = 0; int _currentIndex = 0;
List<Offset> _bullets = []; List<Offset> _bullets = [];
List<Offset> _scaledBullets = []; // Bullets scaled to display target size
int _totalScore = 0; int _totalScore = 0;
Offset? _targetCenter; Offset? _targetCenter;
Uint8List? _targetImageBytes; Uint8List? _targetImageBytes;
@@ -34,20 +36,30 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
int? _highlightedBulletIndex; int? _highlightedBulletIndex;
int? _pressedScoreGroup; int? _pressedScoreGroup;
late String _targetFilePath; late String _targetFilePath;
late String targetAssetPath;
final String targetAssetPath = 'assets/target.png'; late String _selectedTargetPath; // For display
final double bulletSize = 19; double _scaleFactorX = 1.0; // Scale factor for X coordinate
double _scaleFactorY = 1.0; // Scale factor for Y coordinate
Color _bulletColor = Colors.blue; // default/fallback bullet color Color _bulletColor = Colors.blue; // default/fallback bullet color
String _selectedLanguage = 'English'; String _selectedLanguage = 'English';
Color _backgroundColor = Colors.white;
bool _showChartView = false; // Toggle between bullet view and chart view
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBulletColor(); targetAssetPath = 'assets/target.png'; // Default
_loadLanguage(); _initializeScreen();
_loadJsonAndImage(); }
Future<void> _initializeScreen() async {
await Future.wait([
_loadBulletColor(),
_loadLanguage(),
_loadSelectedTarget(),
]);
await _loadJsonAndImage();
} }
String _t(String key) { String _t(String key) {
@@ -61,6 +73,21 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
}); });
} }
Future<void> _loadSelectedTarget() async {
final prefs = await SharedPreferences.getInstance();
final selectedTarget =
prefs.getString('selectedTarget') ?? 'assets/target.png';
// Always use default target for bullet positioning
targetAssetPath = 'assets/target.png';
// Store selected target for display
_selectedTargetPath = selectedTarget;
_backgroundColor = _getBackgroundColorForTarget(selectedTarget);
}
Color _getBackgroundColorForTarget(String targetPath) {
return TargetColors.getBackgroundColor(targetPath);
}
Future<void> _loadBulletColor() async { Future<void> _loadBulletColor() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final selectedColor = prefs.getString('bulletColor') ?? 'Blue'; final selectedColor = prefs.getString('bulletColor') ?? 'Blue';
@@ -107,25 +134,81 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
} }
} }
Future<void> _loadJsonAndImage() async { Future<void> _loadJsonAndImage() async {
try { try {
final byteData = await rootBundle.load(targetAssetPath); // Load default target for bullet positioning reference
final bytes = byteData.buffer.asUint8List(); final defaultByteData = await rootBundle.load(targetAssetPath);
_targetImageDecoded = img.decodeImage(bytes); final defaultBytes = defaultByteData.buffer.asUint8List();
img.Image? defaultTargetDecoded = img.decodeImage(defaultBytes);
if (_targetImageDecoded == null) { if (defaultTargetDecoded == null) {
print("Failed to decode image."); print("Failed to decode default target image.");
return; return;
} }
_targetImageBytes = Uint8List.fromList(img.encodePng(_targetImageDecoded!)); // Calculate target center from default target (for bullet positioning)
_targetCenter = Offset( _targetCenter = Offset(
_targetImageDecoded!.width / 2, defaultTargetDecoded.width / 2,
_targetImageDecoded!.height / 2 - 1, defaultTargetDecoded.height / 2 - 1,
); );
// Load selected target for display
final displayByteData = await rootBundle.load(_selectedTargetPath);
final displayBytes = displayByteData.buffer.asUint8List();
_targetImageDecoded = img.decodeImage(displayBytes);
if (_targetImageDecoded == null) {
print("Failed to decode display target image.");
return;
}
_targetImageBytes =
Uint8List.fromList(img.encodePng(_targetImageDecoded!));
if (widget.isCombinedView) {
// In combined view, collect all bullets from all files
final allBullets = <Offset>[];
String latestDate = 'Unknown Date';
for (var file in widget.jsonFiles) {
final jsonString = await file.readAsString();
final data = jsonDecode(jsonString);
final bulletsJson = data['bullets'] as List?;
if (bulletsJson != null) {
allBullets.addAll(
bulletsJson.map((b) => Offset(b['x'].toDouble(), b['y'].toDouble())).toList(),
);
}
if (data['date'] != null) {
latestDate = data['date'];
}
}
// Parse and convert date format from yyyy-m-d to dd/MM/yyyy
String rawDate = latestDate;
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;
}
_bullets = allBullets;
} else {
// Single file view - load from current file
final currentFile = widget.jsonFiles[_currentIndex]; final currentFile = widget.jsonFiles[_currentIndex];
final jsonString = await currentFile.readAsString(); final jsonString = await currentFile.readAsString();
final data = jsonDecode(jsonString); final data = jsonDecode(jsonString);
@@ -150,9 +233,24 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
_date = rawDate; _date = rawDate;
} }
final bulletsJson = data['bullets'] as List; final bulletsJson = data['bullets'] as List;
_bullets = bulletsJson.map((b) => Offset(b['x'], b['y'])).toList(); _bullets = bulletsJson.map((b) => Offset(b['x'].toDouble(), b['y'].toDouble())).toList();
}
_targetFilePath = currentFile.path; _targetFilePath = widget.isCombinedView
? widget.jsonFiles.first.path
: widget.jsonFiles[_currentIndex].path;
// Calculate scale factors based on image dimensions
_scaleFactorX = _targetImageDecoded!.width / defaultTargetDecoded.width;
_scaleFactorY = _targetImageDecoded!.height / defaultTargetDecoded.height;
// Scale bullets to match display target size
_scaledBullets = _bullets.map((bullet) {
return Offset(
bullet.dx * _scaleFactorX,
bullet.dy * _scaleFactorY,
);
}).toList();
_totalScore = _bullets _totalScore = _bullets
.map((b) => _scoreForBullet(b, _targetCenter!)) .map((b) => _scoreForBullet(b, _targetCenter!))
@@ -194,9 +292,16 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: _backgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text('📅 $_date', style: const TextStyle(color: Colors.white)), title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.calendar_today, size: 20, color: Colors.white),
const SizedBox(width: 8),
Text(_date, style: const TextStyle(color: Colors.white)),
],
),
backgroundColor: Colors.grey[900], backgroundColor: Colors.grey[900],
centerTitle: true, centerTitle: true,
iconTheme: const IconThemeData(color: Colors.white), iconTheme: const IconThemeData(color: Colors.white),
@@ -210,7 +315,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
builder: (BuildContext context) { builder: (BuildContext context) {
return Dialog( return Dialog(
backgroundColor: Colors.grey[900], backgroundColor: Colors.grey[900],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
child: Column( child: Column(
@@ -229,7 +335,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
_t("Are you sure you want to delete this target?"), _t("Are you sure you want to delete this target?"),
style: const TextStyle(color: Colors.white70, fontSize: 16), style: const TextStyle(
color: Colors.white70, fontSize: 16),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -245,7 +352,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent, backgroundColor: Colors.redAccent,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14), padding:
const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -263,7 +371,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[800], backgroundColor: Colors.grey[800],
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14), padding:
const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -277,13 +386,14 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
); );
}, },
); );
}, },
), ),
], ],
), ),
body: _targetImageDecoded == null body: _targetImageDecoded == null
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _showChartView
? _buildChartView()
: Column( : Column(
children: [ children: [
Expanded( Expanded(
@@ -313,6 +423,10 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
final scaleX = displayWidth / imgWidth; final scaleX = displayWidth / imgWidth;
final scaleY = displayHeight / imgHeight; final scaleY = displayHeight / imgHeight;
// Scale bullet size by target scale factors and display scale factors
final dynamicBulletSize =
(((_scaleFactorX + _scaleFactorY) / 2) * 53) *
((scaleX + scaleY) / 2);
return Stack( return Stack(
children: [ children: [
@@ -321,36 +435,42 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
top: offsetY, top: offsetY,
width: displayWidth, width: displayWidth,
height: displayHeight, height: displayHeight,
child: Image.memory(_targetImageBytes!, fit: BoxFit.fill), child: Image.memory(_targetImageBytes!,
fit: BoxFit.fill),
), ),
..._bullets.asMap().entries.map((entry) { ..._scaledBullets.asMap().entries.map((entry) {
final i = entry.key; final i = entry.key;
final bullet = entry.value; final scaledBullet = entry.value;
final dx = bullet.dx * scaleX + offsetX; // Scaled bullets are already scaled to display target dimensions
final dy = bullet.dy * scaleY + offsetY; final dx = scaledBullet.dx * scaleX + offsetX;
final score = _scoreForBullet(bullet, _targetCenter ?? Offset.zero); final dy = scaledBullet.dy * scaleY + offsetY;
// Score is calculated using original bullet position and default target center
final originalBullet = _bullets[i];
final score = _scoreForBullet(
originalBullet, _targetCenter ?? Offset.zero);
final isHighlighted = widget.isCombinedView final isHighlighted = widget.isCombinedView
? (_pressedScoreGroup == score) ? (_pressedScoreGroup == score)
: (i == _highlightedBulletIndex); : (i == _highlightedBulletIndex);
return Positioned( return Positioned(
left: dx - bulletSize / 2, left: dx - dynamicBulletSize / 2,
top: dy - bulletSize / 2, top: dy - dynamicBulletSize / 2,
child: Container( child: Container(
width: bulletSize, width: dynamicBulletSize,
height: bulletSize, height: dynamicBulletSize,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _bulletColor.withOpacity(0.8), color: _bulletColor.withValues(alpha: 0.8),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: isHighlighted ? Colors.red : Colors.black, color: isHighlighted
? Colors.red
: Colors.black,
width: isHighlighted ? 3 : 1, width: isHighlighted ? 3 : 1,
), ),
), ),
), ),
); );
}) }).toList(),
.toList(),
], ],
); );
}, },
@@ -358,8 +478,9 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Container( Container(
color: Colors.white, color: _backgroundColor,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row( child: Row(
children: widget.isCombinedView children: widget.isCombinedView
? _buildGroupedScoreButtons() ? _buildGroupedScoreButtons()
@@ -373,35 +494,179 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
color: Colors.grey[900], color: Colors.grey[900],
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: SizedBox( child: SizedBox(
height: 150, height: 60,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Total score
Row(
children: [
const Icon(Icons.track_changes,
size: 20, color: Colors.white),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Combined badge with text and score Text(
Container( _t('Total score'),
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( style: const TextStyle(
fontSize: 28, fontSize: 11,
color: Colors.white54,
fontWeight: FontWeight.w500,
),
),
Text(
"$_totalScore",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
), ),
), ),
), ],
), ),
], ],
), ),
// Chart toggle button
IconButton(
onPressed: () {
setState(() {
_showChartView = !_showChartView;
});
},
icon: Icon(
_showChartView ? Icons.bar_chart : Icons.image,
color: Colors.white,
size: 24,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 24,
),
],
),
),
),
),
);
}
Widget _buildChartView() {
if (widget.isCombinedView) {
// Folder view: each target position as a bar with combined scores
return _buildFolderChartView();
} else {
// Single file view: each shot as a bar
return _buildSingleFileChartView();
}
}
Widget _buildFolderChartView() {
// Calculate target scores from all files
// Each file represents one target at position (fileIndex + 1)
final Map<int, int> targetScores = {};
for (int fileIdx = 0; fileIdx < widget.jsonFiles.length; fileIdx++) {
final file = widget.jsonFiles[fileIdx];
final targetPosition = fileIdx + 1; // T1, T2, T3, etc.
final data = jsonDecode(file.readAsStringSync());
final bullets = (data['bullets'] as List?) ?? [];
// Sum scores for this target
int targetScore = 0;
for (int j = 0; j < bullets.length; j++) {
final bullet = Offset(bullets[j]['x'].toDouble(), bullets[j]['y'].toDouble());
final score = _scoreForBullet(bullet, _targetCenter ?? Offset.zero);
targetScore += score;
}
targetScores[targetPosition] = targetScore;
}
if (targetScores.isEmpty) {
return Center(
child: Text(
_t('No data available'),
style: const TextStyle(color: Colors.white70),
),
);
}
final sortedTargets = targetScores.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
final maxScore = sortedTargets.map((e) => e.value).reduce((a, b) => a > b ? a : b).toDouble();
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Card(
color: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: CustomPaint(
painter: BarChartPainter(
data: sortedTargets,
maxScore: maxScore,
bulletColor: _bulletColor,
backgroundColor: Colors.white,
selectedLanguage: _selectedLanguage,
translateFunc: _t,
),
size: Size.infinite,
),
),
),
);
}
Widget _buildSingleFileChartView() {
// Single file view: each shot as a bar
final Map<int, int> shotScores = {};
for (int i = 0; i < _bullets.length; i++) {
final score = _scoreForBullet(_bullets[i], _targetCenter ?? Offset.zero);
shotScores[i + 1] = score; // Shot number starts at 1
}
if (shotScores.isEmpty) {
return Center(
child: Text(
_t('No data available'),
style: const TextStyle(color: Colors.white70),
),
);
}
final sortedShots = shotScores.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
final maxScore = sortedShots.map((e) => e.value).reduce((a, b) => a > b ? a : b).toDouble();
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Card(
color: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: CustomPaint(
painter: BarChartPainter(
data: sortedShots,
maxScore: maxScore,
bulletColor: _bulletColor,
isShotView: true,
backgroundColor: Colors.white,
selectedLanguage: _selectedLanguage,
translateFunc: _t,
xAxisLabelKey: 'Points',
),
size: Size.infinite,
),
), ),
), ),
); );
@@ -470,7 +735,7 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: _pressedScoreGroup == score color: _pressedScoreGroup == score
? Colors.red[400] ? Colors.red[400]
: _bulletColor.withOpacity(0.85), : _bulletColor.withValues(alpha: 0.85),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16), bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16),
@@ -499,8 +764,6 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
}).toList(); }).toList();
} }
List<Widget> _buildIndividualScoreButtons() { List<Widget> _buildIndividualScoreButtons() {
final scoredBullets = _bullets.asMap().entries.map((entry) { final scoredBullets = _bullets.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
@@ -510,7 +773,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
}).toList(); }).toList();
// Sort descending by score // Sort descending by score
scoredBullets.sort((a, b) => (b['score'] as int).compareTo(a['score'] as int)); scoredBullets
.sort((a, b) => (b['score'] as int).compareTo(a['score'] as int));
return scoredBullets.map((entry) { return scoredBullets.map((entry) {
final int index = entry['index'] as int; final int index = entry['index'] as int;
@@ -561,7 +825,7 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: _highlightedBulletIndex == index color: _highlightedBulletIndex == index
? Colors.red[400] ? Colors.red[400]
: _bulletColor.withOpacity(0.85), : _bulletColor.withValues(alpha: 0.85),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16), bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16),
@@ -586,5 +850,239 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
); );
}).toList(); }).toList();
} }
}
class BarChartPainter extends CustomPainter {
final List<MapEntry<int, int>> data;
final double maxScore;
final Color bulletColor;
final bool isShotView;
final Color backgroundColor;
final String selectedLanguage;
final Function(String) translateFunc;
final String? xAxisLabelKey;
BarChartPainter({
required this.data,
required this.maxScore,
required this.bulletColor,
this.isShotView = false,
this.backgroundColor = Colors.white,
this.selectedLanguage = 'English',
required this.translateFunc,
this.xAxisLabelKey,
});
@override
void paint(Canvas canvas, Size size) {
const leftLabelWidth = 50.0;
const padding = 24.0;
const bottomPadding = 35.0;
final chartWidth = size.width - leftLabelWidth - padding;
final chartHeight = size.height - padding - bottomPadding;
// Determine if background is light or dark
final isLightBackground = _isLightColor(backgroundColor);
final axisColor = isLightBackground ? Colors.black54 : Colors.white54;
final gridColor = isLightBackground
? Colors.black.withValues(alpha: 0.08)
: Colors.white.withValues(alpha: 0.08);
final labelColor = isLightBackground ? Colors.black87 : Colors.white70;
// Draw Y axis (left) - thinner
canvas.drawLine(
Offset(leftLabelWidth, padding),
Offset(leftLabelWidth, size.height - bottomPadding),
Paint()
..color = axisColor
..strokeWidth = 1,
);
// Draw X axis (bottom) - thinner
canvas.drawLine(
Offset(leftLabelWidth, size.height - bottomPadding),
Offset(size.width - padding, size.height - bottomPadding),
Paint()
..color = axisColor
..strokeWidth = 1,
);
// Draw Y axis label (Score)
final yAxisLabel = TextPainter(
text: TextSpan(
text: translateFunc('Score'),
style: TextStyle(color: axisColor, fontSize: 10),
),
textDirection: TextDirection.ltr,
);
yAxisLabel.layout();
yAxisLabel.paint(
canvas,
Offset(8, padding - 10),
);
// Draw X axis label (Targets, Shots, or Points)
late String xAxisLabelText;
if (xAxisLabelKey != null) {
xAxisLabelText = translateFunc(xAxisLabelKey!);
} else {
xAxisLabelText = isShotView ? translateFunc('Shots') : translateFunc('Targets');
}
final xAxisLabel = TextPainter(
text: TextSpan(
text: xAxisLabelText,
style: TextStyle(color: axisColor, fontSize: 10),
),
textDirection: TextDirection.ltr,
);
xAxisLabel.layout();
xAxisLabel.paint(
canvas,
Offset(size.width - padding - 40, size.height - 17),
);
// Draw grid lines and X axis labels (score values)
for (int i = 0; i <= 10; i++) {
final xValue = (maxScore / 10) * i;
final xPos = leftLabelWidth + (chartWidth * i / 10);
// Grid line (lighter)
canvas.drawLine(
Offset(xPos, padding),
Offset(xPos, size.height - bottomPadding),
Paint()
..color = gridColor
..strokeWidth = 0.5,
);
// X axis label (score) - smaller and lighter
if (i % 2 == 0) { // Only show every 2nd label to avoid clutter
final textPainter = TextPainter(
text: TextSpan(
text: '${xValue.toInt()}',
style: TextStyle(color: labelColor.withValues(alpha: 0.6), fontSize: 10),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(xPos - textPainter.width / 2, size.height - bottomPadding + 8),
);
}
}
// Draw horizontal bars
if (data.isNotEmpty) {
final barHeight = chartHeight / (data.length * 1.3);
final spacing = barHeight * 0.3;
for (int i = 0; i < data.length; i++) {
final entry = data[i];
final targetNum = entry.key;
final score = entry.value;
final barWidth = (score / maxScore) * chartWidth;
final yPos = padding + (i * (barHeight + spacing)) + spacing;
final xStart = leftLabelWidth;
// Draw bar with subtle shadow effect
final shadowPaint = Paint()
..color = Colors.black.withValues(alpha: 0.1)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(
xStart + 1,
yPos + 1,
barWidth,
barHeight,
),
const Radius.circular(6),
),
shadowPaint,
);
// Draw main bar with rounded corners
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(
xStart,
yPos,
barWidth,
barHeight,
),
const Radius.circular(6),
),
Paint()..color = bulletColor,
);
// Draw Y axis label (target name)
final label = isShotView ? 'S$targetNum' : 'T$targetNum';
final labelPainter = TextPainter(
text: TextSpan(
text: label,
style: TextStyle(
color: labelColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
textDirection: TextDirection.ltr,
);
labelPainter.layout();
labelPainter.paint(
canvas,
Offset(
leftLabelWidth - labelPainter.width - 10,
yPos + barHeight / 2 - labelPainter.height / 2,
),
);
// Draw score at end of bar (only if bar is wide enough)
if (barWidth > 20) {
final scorePainter = TextPainter(
text: TextSpan(
text: '${score.toInt()}',
style: TextStyle(
color: isLightBackground ? Colors.black87 : Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
scorePainter.layout();
scorePainter.paint(
canvas,
Offset(
xStart + barWidth + 8,
yPos + barHeight / 2 - scorePainter.height / 2,
),
);
}
}
}
}
@override
bool shouldRepaint(BarChartPainter oldDelegate) {
return oldDelegate.data != data ||
oldDelegate.maxScore != maxScore ||
oldDelegate.bulletColor != bulletColor ||
oldDelegate.backgroundColor != backgroundColor ||
oldDelegate.isShotView != isShotView ||
oldDelegate.selectedLanguage != selectedLanguage ||
oldDelegate.xAxisLabelKey != xAxisLabelKey;
}
bool _isLightColor(Color color) {
// Calculate luminance to determine if background is light or dark
final r = (color.r * 255.0).round();
final g = (color.g * 255.0).round();
final b = (color.b * 255.0).round();
final luminance = (r * 299 + g * 587 + b * 114) / 1000;
return luminance > 128;
}
} }
+14 -4
View File
@@ -83,11 +83,21 @@ class YOLODetector {
if (score < confidenceThreshold) continue; if (score < confidenceThreshold) continue;
// The model outputs coordinates normalized to [0, 1] relative to the input tensor (inputWidth x inputHeight)
// We need to scale back to the original cropped image coordinates
// The cropped image (cropWidth x cropHeight) was resized to (inputWidth x inputHeight)
final scaledX1 = x1 * croppedResult.cropWidth;
final scaledY1 = y1 * croppedResult.cropHeight;
final scaledX2 = x2 * croppedResult.cropWidth;
final scaledY2 = y2 * croppedResult.cropHeight;
// Add offset to get coordinates in the original image space
results.add(DetectionResult( results.add(DetectionResult(
x1 * croppedResult.cropWidth + croppedResult.offsetX, scaledX1 + croppedResult.offsetX,
y1 * croppedResult.cropHeight + croppedResult.offsetY, scaledY1 + croppedResult.offsetY,
x2 * croppedResult.cropWidth + croppedResult.offsetX, scaledX2 + croppedResult.offsetX,
y2 * croppedResult.cropHeight + croppedResult.offsetY, scaledY2 + croppedResult.offsetY,
score, score,
classId, classId,
)); ));
+169
View File
@@ -0,0 +1,169 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/google_calendar_service.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin();
bool _initialized = false;
static const _channel = AndroidNotificationChannel(
'shooting_app_events',
'Event Reminders',
description: 'Notifications for upcoming shooting events',
importance: Importance.max,
playSound: true,
enableVibration: true,
);
NotificationDetails get _notifDetails => const NotificationDetails(
android: AndroidNotificationDetails(
'shooting_app_events',
'Event Reminders',
channelDescription: 'Notifications for upcoming shooting events',
importance: Importance.max,
priority: Priority.high,
icon: '@drawable/ic_stat_icon_white',
),
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
);
Future<void> initialize() async {
if (_initialized) return;
_initialized = true; // set early to prevent re-entry
try {
tz.initializeTimeZones();
final String timezoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timezoneName));
} catch (_) {}
try {
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initSettings = InitializationSettings(android: androidSettings, iOS: iosSettings);
await _plugin.initialize(initSettings);
final androidPlugin = _plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
await androidPlugin?.createNotificationChannel(_channel);
await requestPermissions();
} catch (_) {}
}
Future<bool> requestPermissions() async {
final android = _plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
final ios = _plugin.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>();
bool granted = false;
if (android != null) {
granted = await android.requestNotificationsPermission() ?? false;
}
if (ios != null) {
granted = await ios.requestPermissions(alert: true, badge: true, sound: true) ?? false;
}
return granted;
}
Future<void> cancelAll() async {
await _plugin.cancelAll();
}
Future<void> scheduleEventNotifications(List<CalendarEvent> events) async {
try {
await initialize();
final prefs = await SharedPreferences.getInstance();
final hours = prefs.getInt('notificationHours_single') ?? 1;
final notifyDezuranje = prefs.getBool('notifyDezuranje') ?? true;
final notifySestanek = prefs.getBool('notifySestanek') ?? true;
final notifyTekma = prefs.getBool('notifyTekma') ?? true;
final isSlovenian = (prefs.getString('language') ?? 'English') == 'Slovenian';
final now = DateTime.now();
await _plugin.cancelAll();
int notifId = 0;
for (final event in events) {
if (event.startTime == null) continue;
final summary = (event.summary ?? '').toLowerCase();
final isCompetition = summary.contains('liga') || summary.contains('tekmovanje');
final isSestanek = summary.contains('sestanek');
final isDezuranje = !isCompetition && !isSestanek;
if (isCompetition && !notifyTekma) continue;
if (isSestanek && !notifySestanek) continue;
if (isDezuranje && !notifyDezuranje) continue;
final notifyAt = event.startTime!.subtract(Duration(hours: hours));
if (notifyAt.isAfter(now)) {
await _scheduleNotification(
id: notifId++,
title: event.summary ?? 'Event',
body: isSlovenian
? (hours == 1 ? 'Začne se čez 1 uro' : 'Začne se čez $hours ur')
: (hours == 1 ? 'Starts in 1 hour' : 'Starts in $hours hours'),
scheduledDate: notifyAt,
);
}
}
} catch (_) {}
}
Future<void> scheduleTestNotification() async {
try {
await initialize();
final prefs = await SharedPreferences.getInstance();
final isSlovenian = (prefs.getString('language') ?? 'English') == 'Slovenian';
await _plugin.show(
9999,
isSlovenian ? 'Testno obvestilo' : 'Test Notification',
isSlovenian ? 'Obvestila delujejo!' : 'Notifications are working!',
_notifDetails,
);
final in10s = DateTime.now().add(const Duration(seconds: 10));
await _scheduleNotification(
id: 9998,
title: isSlovenian ? 'Načrtovano testno obvestilo' : 'Scheduled Test',
body: isSlovenian ? 'Načrtovana obvestila delujejo!' : 'Scheduled notifications are working!',
scheduledDate: in10s,
);
} catch (_) {}
}
Future<void> _scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
}) async {
final tzDate = tz.TZDateTime.from(scheduledDate, tz.local);
await _plugin.zonedSchedule(
id,
title,
body,
tzDate,
_notifDetails,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
}
-73
View File
@@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:upgrader/upgrader.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/translations.dart';
import '../widgets/custom_upgrade_alert.dart';
// GitHub Configuration
const String _githubRepoOwner = 'Bl3kiiie';
const String _githubRepoName = 'shooting_app';
const String _githubToken =
'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'; // Replace with your GitHub Personal Access Token
class UpdateChecker {
static final UpdateChecker _instance = UpdateChecker._internal();
factory UpdateChecker() {
return _instance;
}
UpdateChecker._internal();
Future<void> checkForUpdate(BuildContext context, String language) async {
// Check if we've already checked today
final prefs = await SharedPreferences.getInstance();
final lastCheckTime = prefs.getInt('last_update_check_time') ?? 0;
final now = DateTime.now().millisecondsSinceEpoch;
final oneDayInMilliseconds = 24 * 60 * 60 * 1000;
// Only check once per day
if (now - lastCheckTime < oneDayInMilliseconds) {
return;
}
// Update the last check time
await prefs.setInt('last_update_check_time', now);
final upgrader = Upgrader();
try {
// Check if update is available
final isUpdateAvailable = await upgrader.isUpdateAvailable();
if (isUpdateAvailable && context.mounted) {
_showCustomUpdateDialog(context, language);
}
} catch (e) {
// Silently fail if update check fails
print('Update check failed: $e');
}
}
void _showCustomUpdateDialog(BuildContext context, String language) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return CustomUpdateDialog(
title: AppTranslations.translate('Update Available', language),
message: AppTranslations.translate(
'A new version of the app is available', language),
updateButtonText: AppTranslations.translate('Update', language),
laterButtonText: AppTranslations.translate('Later', language),
onUpdate: () {
Upgrader().sendUserToAppStore();
},
onLater: () {
Navigator.of(context).pop();
},
);
},
);
}
}
+46
View File
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
class TargetColors {
static const Map<String, Color> targetBackgroundColors = {
'assets/target.png': Color(0xFFFFFFFF), // White
'assets/targets/Target1.jpg': Color(0xFFE8ECD5), // Light beige
'assets/targets/Target2.jpg': Color(0xFFC1DF87), // Light green
'assets/targets/Target3.jpg': Color(0xFFFEFEFE), // Off-white
'assets/targets/Target4.jpg': Color(0xFFD42931), // Red
'assets/targets/Target5.jpg': Color(0xFF02050A), // Very dark blue/black
'assets/targets/Target6.jpg': Color(0xFFF3E7D1), // Light beige
'assets/targets/Target7.jpg': Color(0xFFFFD454), // Bright yellow
'assets/targets/Target8.jpg': Color(0xFF1E5E81), // Dark teal blue
'assets/targets/Target9.jpg': Color(0xFFFEFEFC), // Off-white
'assets/targets/Target10.jpg': Color(0xFF98926E), // Olive green
'assets/targets/Target11.jpg': Color(0xFF13132B), // Dark blue
'assets/targets/Target12.jpg': Color(0xFFF5F4EF), // Off-white
'assets/targets/Target13.jpg': Color(0xFF174AA5), // Blue
'assets/targets/Target14.jpg': Color(0xFFFEE05E), // Light yellow
'assets/targets/Target15.jpg': Color(0xFFFFFFFF), // White
'assets/targets/Target16.jpg': Color(0xFF000000), // Black
'assets/targets/Target17.jpg': Color(0xFF21304D), // Dark blue
'assets/targets/Target18.jpg': Color(0xFF050608), // Very dark gray
'assets/targets/Target19.jpg': Color(0xFFF68527), // Warm orange
'assets/targets/Target20.jpg': Color(0xFFA9D9EF), // Light blue
'assets/targets/Target21.jpg': Color(0xFFDA6868), // Muted red
'assets/targets/Target22.jpg': Color(0xFFFBFAF6), // Off-white
'assets/targets/Target23.jpg': Color(0xFF000000), // Black
'assets/targets/Target24.jpg': Color(0xFF646462), // Grey
'assets/targets/Target25.jpg': Color(0xFF040308), // Very dark gray
'assets/targets/Target26.jpg': Color(0xFF161C2C), // Dark blue-gray
'assets/targets/Target27.jpg': Color(0xFF28A5DF), // Blue
'assets/targets/Target28.jpg': Color(0xFFFFFFFF), // White
'assets/targets/Target29.jpg': Color(0xFF180E27), // Dark purple
'assets/targets/Target30.jpg': Color(0xFFFDFDFB), // Off-white
'assets/targets/Target31.jpg': Color(0xFFC52736), // Red
'assets/targets/Target32.jpg': Color(0xFFFCF3EE), // Light cream
'assets/targets/Target33.jpg': Color(0xFFFCFCF3), // Very light off-white
'assets/targets/Target34.jpg': Color(0xFFFFFFFD), // Almost white
'assets/targets/Target35.jpg': Color(0xFFFFC5C4), // Light salmon
};
static Color getBackgroundColor(String targetPath) {
return targetBackgroundColors[targetPath] ?? Colors.grey[900]!;
}
}
+306 -34
View File
@@ -18,12 +18,132 @@ class AppTranslations {
'Slovenian': 'Slovenščina', 'Slovenian': 'Slovenščina',
}, },
// Appearance Settings
'Appearance': {
'English': 'Appearance',
'Slovenian': 'Videz',
},
'Appearance Settings': {
'English': 'Appearance Settings',
'Slovenian': 'Nastavitve videza',
},
'Bullet': {
'English': 'Bullet',
'Slovenian': 'Zadetek',
},
// Bullet color // Bullet color
'Select Bullet Color': { 'Select Bullet Color': {
'English': 'Select Bullet Color', 'English': 'Select Bullet Color',
'Slovenian': 'Izberi barvo zadetka', 'Slovenian': 'Izberi barvo zadetka',
}, },
// Target selection
'Select Target': {
'English': 'Select Target',
'Slovenian': 'Izberi tarčo',
},
'Default Target': {
'English': 'Default Target',
'Slovenian': 'Privzeta tarča',
},
'Target1': {
'English': 'Target 1',
'Slovenian': 'Tarča 1',
},
'Target2': {
'English': 'Target 2',
'Slovenian': 'Tarča 2',
},
'Target3': {
'English': 'Target 3',
'Slovenian': 'Tarča 3',
},
'Target4': {
'English': 'Target 4',
'Slovenian': 'Tarča 4',
},
'Target5': {
'English': 'Target 5',
'Slovenian': 'Tarča 5',
},
'Target6': {
'English': 'Target 6',
'Slovenian': 'Tarča 6',
},
'Target7': {
'English': 'Target 7',
'Slovenian': 'Tarča 7',
},
'Target8': {
'English': 'Target 8',
'Slovenian': 'Tarča 8',
},
'Target9': {
'English': 'Target 9',
'Slovenian': 'Tarča 9',
},
'Target10': {
'English': 'Target 10',
'Slovenian': 'Tarča 10',
},
'Target11': {
'English': 'Target 11',
'Slovenian': 'Tarča 11',
},
'Target12': {
'English': 'Target 12',
'Slovenian': 'Tarča 12',
},
'Target13': {
'English': 'Target 13',
'Slovenian': 'Tarča 13',
},
'Target14': {
'English': 'Target 14',
'Slovenian': 'Tarča 14',
},
'Target15': {
'English': 'Target 15',
'Slovenian': 'Tarča 15',
},
'Target16': {
'English': 'Target 16',
'Slovenian': 'Tarča 16',
},
'Target17': {
'English': 'Target 17',
'Slovenian': 'Tarča 17',
},
'Target18': {
'English': 'Target 18',
'Slovenian': 'Tarča 18',
},
'Target19': {
'English': 'Target 19',
'Slovenian': 'Tarča 19',
},
'Target20': {
'English': 'Target 20',
'Slovenian': 'Tarča 20',
},
'Target21': {
'English': 'Target 21',
'Slovenian': 'Tarča 21',
},
'Target22': {
'English': 'Target 22',
'Slovenian': 'Tarča 22',
},
'Target23': {
'English': 'Target 23',
'Slovenian': 'Tarča 23',
},
'Target24': {
'English': 'Target 24',
'Slovenian': 'Tarča 24',
},
// Camera settings // Camera settings
'Camera Settings': { 'Camera Settings': {
'English': 'Camera Settings', 'English': 'Camera Settings',
@@ -187,10 +307,6 @@ class AppTranslations {
'English': 'Tournament', 'English': 'Tournament',
'Slovenian': 'Tekmovanje', 'Slovenian': 'Tekmovanje',
}, },
'Last used folder': {
'English': 'Last used folder',
'Slovenian': 'Zadnja uporabljena mapa',
},
'New folder': { 'New folder': {
'English': 'New folder', 'English': 'New folder',
'Slovenian': 'Nova mapa', 'Slovenian': 'Nova mapa',
@@ -221,10 +337,18 @@ class AppTranslations {
'English': 'Tournament Mode 🏆', 'English': 'Tournament Mode 🏆',
'Slovenian': 'Tekmovalni način 🏆', 'Slovenian': 'Tekmovalni način 🏆',
}, },
'Tournament Mode': {
'English': 'Tournament Mode',
'Slovenian': 'Tekmovalni način',
},
'Mark as Tournament 🏆': { 'Mark as Tournament 🏆': {
'English': 'Mark as Tournament 🏆', 'English': 'Mark as Tournament 🏆',
'Slovenian': 'Označi kot tekmovanje 🏆', 'Slovenian': 'Označi kot tekmovanje 🏆',
}, },
'Mark as Tournament': {
'English': 'Mark as Tournament',
'Slovenian': 'Označi kot tekmovanje',
},
'Apply': { 'Apply': {
'English': 'Apply', 'English': 'Apply',
'Slovenian': 'Uporabi', 'Slovenian': 'Uporabi',
@@ -331,6 +455,10 @@ class AppTranslations {
'English': 'Total score', 'English': 'Total score',
'Slovenian': 'Rezultat:', 'Slovenian': 'Rezultat:',
}, },
'Total score:': {
'English': 'Total score:',
'Slovenian': 'Skupni seštevek:',
},
// Stream Screen (Camera View) // Stream Screen (Camera View)
'Choose a Filter': { 'Choose a Filter': {
@@ -584,45 +712,189 @@ class AppTranslations {
'English': 'copied to clipboard', 'English': 'copied to clipboard',
'Slovenian': 'kopirano v odložišče', 'Slovenian': 'kopirano v odložišče',
}, },
'Select Cameras to Spectate': {
// App Update translations 'English': 'Select Cameras to Spectate',
'Update Available': { 'Slovenian': 'Izberi kamere za ogled',
'English': 'Update Available',
'Slovenian': 'Posodobitev je na voljo',
}, },
'A new version of the app is available': { 'Choose 1-6 cameras': {
'English': 'A new version of the app is available', 'English': 'Choose 1-6 cameras',
'Slovenian': 'Nova različica aplikacije je na voljo', 'Slovenian': 'Izberi 1-6 kamer',
}, },
'Update': { 'No cameras configured': {
'English': 'Update', 'English': 'No cameras configured',
'Slovenian': 'Posodobi', 'Slovenian': 'Ni nastavljenih kamer',
}, },
'Later': { 'View Cameras': {
'English': 'Later', 'English': 'View Cameras',
'Slovenian': 'Kasneje', 'Slovenian': 'Prikaži kamere',
},
'Multi-Camera View': {
'English': 'Multi-Camera View',
'Slovenian': 'Več-kamerski pogled',
},
'Tap to return to grid': {
'English': 'Tap to return to grid',
'Slovenian': 'Tapni za vrnitev na mrežo',
},
'Stream unavailable': {
'English': 'Stream unavailable',
'Slovenian': 'Pretok ni na voljo',
},
'Select at least one camera': {
'English': 'Select at least one camera',
'Slovenian': 'Izberi vsaj eno kamero',
},
'Spectate': {
'English': 'Spectate',
'Slovenian': 'Opazuj',
},
'Select Cameras': {
'English': 'Select Cameras',
'Slovenian': 'Izberi kamere',
},
'Spectator Mode': {
'English': 'Spectator Mode',
'Slovenian': 'Način opazovanja',
},
'Please ensure:': {
'English': 'Please ensure:',
'Slovenian': 'Prepričaj se:',
},
'WiFi is enabled': {
'English': 'WiFi is enabled',
'Slovenian': 'WiFi je vključen',
},
'Mobile data is disabled': {
'English': 'Mobile data is disabled',
'Slovenian': 'Mobilni podatki so onemogočeni',
},
'Reload': {
'English': 'Reload',
'Slovenian': 'Ponovno naloži',
}, },
// App Update Check Button // Analytics
'Check for Updates': { 'Analytics': {
'English': 'Check for Updates', 'English': 'Analytics',
'Slovenian': 'Preveri za posodobitve', 'Slovenian': 'Analitika',
}, },
'No updates available': { 'Compare Sessions': {
'English': 'No updates available', 'English': 'Compare Sessions',
'Slovenian': 'Ni dostopnih posodobitev', 'Slovenian': 'Primerjaj treninge',
}, },
'You are using the latest version': { 'Select 2-4 sessions': {
'English': 'You are using the latest version', 'English': 'Select 2-4 sessions to compare',
'Slovenian': 'Uporabljate najnovejšo različico', 'Slovenian': 'Izberi 2-4 treninge za primerjavo',
}, },
'Updates': { 'Select at least 2 sessions': {
'English': 'Updates', 'English': 'Select at least 2 sessions',
'Slovenian': 'Posodobitve', 'Slovenian': 'Izberi vsaj 2 treninga',
}, },
'OK': { 'Max 4 sessions': {
'English': 'OK', 'English': 'Maximum 4 sessions can be compared',
'Slovenian': 'V redu', 'Slovenian': 'Primerjati je mogoče največ 4 treninge',
},
'No sessions available': {
'English': 'No sessions available',
'Slovenian': 'Ni razpoložljivih treningov',
},
'No data for target': {
'English': 'No data for this target',
'Slovenian': 'Ni podatkov za to tarčo',
},
'Training View': {
'English': 'Training View',
'Slovenian': 'Pogled treninga',
},
'Training Comparison': {
'English': 'Training Comparison',
'Slovenian': 'Primerjava treningov',
},
'Tournament Comparison': {
'English': 'Tournament Comparison',
'Slovenian': 'Primerjava tekmovanj',
},
'Compare with': {
'English': 'Compare with',
'Slovenian': 'Primerjaj s',
},
'No folder selected': {
'English': 'No folder selected',
'Slovenian': 'Ni izbrane mape',
},
'Select': {
'English': 'Select',
'Slovenian': 'Izberi',
},
'Clear': {
'English': 'Clear',
'Slovenian': 'Počisti',
},
'Score': {
'English': 'Score',
'Slovenian': 'Rezultat',
},
'Targets': {
'English': 'Targets',
'Slovenian': 'Tarče',
},
'Shots': {
'English': 'Shots',
'Slovenian': 'Streli',
},
'Points': {
'English': 'Points',
'Slovenian': 'Točke',
},
// Notifications
'Notifications': {
'English': 'Notifications',
'Slovenian': 'Obvestila',
},
'Event Reminders': {
'English': 'Event Reminders',
'Slovenian': 'Opomniki za dogodke',
},
'Notify before event': {
'English': 'Notify before event',
'Slovenian': 'Obvesti pred dogodkom',
},
'Hours before': {
'English': 'Hours before',
'Slovenian': 'Ur pred dogodkom',
},
'hour': {
'English': 'hour',
'Slovenian': 'ura',
},
'hours': {
'English': 'hours',
'Slovenian': 'ur',
},
'Notifications enabled': {
'English': 'Notifications enabled',
'Slovenian': 'Obvestila vključena',
},
'Notifications disabled': {
'English': 'Notifications disabled',
'Slovenian': 'Obvestila izključena',
},
'Event types': {
'English': 'Event types',
'Slovenian': 'Vrste dogodkov',
},
'Dežuranje': {
'English': 'Duty shifts',
'Slovenian': 'Dežuranje',
},
'Sestanek': {
'English': 'Meetings',
'Slovenian': 'Sestanki',
},
'Tekma': {
'English': 'Competitions',
'Slovenian': 'Tekme',
}, },
}; };
-155
View File
@@ -1,155 +0,0 @@
import 'package:flutter/material.dart';
import 'package:upgrader/upgrader.dart';
import '../utils/translations.dart';
class CustomUpgradeAlert extends StatefulWidget {
final Widget child;
final String language;
const CustomUpgradeAlert({
super.key,
required this.child,
required this.language,
});
@override
State<CustomUpgradeAlert> createState() => _CustomUpgradeAlertState();
}
class _CustomUpgradeAlertState extends State<CustomUpgradeAlert> {
@override
Widget build(BuildContext context) {
return UpgradeAlert(
child: widget.child,
upgrader: Upgrader(
debugDisplayAlways: true,
),
dialogStyle: UpgradeDialogStyle.material,
shouldPopScope: () => true,
);
}
}
class CustomUpdateDialog extends StatelessWidget {
final String title;
final String message;
final String updateButtonText;
final String laterButtonText;
final VoidCallback onUpdate;
final VoidCallback onLater;
const CustomUpdateDialog({
super.key,
required this.title,
required this.message,
required this.updateButtonText,
required this.laterButtonText,
required this.onUpdate,
required this.onLater,
});
@override
Widget build(BuildContext 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: [
// App Icon
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.update,
color: Colors.white,
size: 40,
),
),
const SizedBox(height: 24),
// Title
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Message
Text(
message,
style: TextStyle(
fontSize: 16,
color: Colors.grey[300],
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Buttons
Row(
children: [
// Later Button
Expanded(
child: GestureDetector(
onTap: onLater,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
laterButtonText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
),
),
),
const SizedBox(width: 12),
// Update Button
Expanded(
child: GestureDetector(
onTap: onUpdate,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
updateButtonText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
],
),
],
),
),
);
}
}
+61 -61
View File
@@ -85,10 +85,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -137,14 +137,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -165,10 +157,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -254,6 +246,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
url: "https://pub.dev"
source: hosted
version: "18.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
flutter_mjpeg: flutter_mjpeg:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -283,6 +299,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_timezone:
dependency: "direct main"
description:
name: flutter_timezone
sha256: ea53c61c9152f271a5e30624a624184804947b6a733ff2b64186bb2579446892
url: "https://pub.dev"
source: hosted
version: "3.0.1"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -352,14 +376,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "1.6.0"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
@@ -468,26 +484,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "11.0.2"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.9" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -500,26 +516,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -528,14 +544,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
os_detect:
dependency: transitive
description:
name: os_detect
sha256: "7d87c0dd98c6faf110d5aa498e9a6df02ffce4bb78cc9cfc8ad02929be9bb71f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
package_info_plus: package_info_plus:
dependency: transitive dependency: transitive
description: description:
@@ -801,10 +809,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" version: "0.7.10"
tflite_flutter: tflite_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -813,6 +821,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.0" version: "0.11.0"
timezone:
dependency: "direct main"
description:
name: timezone
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -821,14 +837,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
upgrader:
dependency: "direct main"
description:
name: upgrader
sha256: "4ba3f840691ffa43c6d71a93a6ad3ae01590c7895c3eb33f9f683513f736a9f7"
url: "https://pub.dev"
source: hosted
version: "12.3.0"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -921,18 +929,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
version:
dependency: transitive
description:
name: version
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
visibility_detector: visibility_detector:
dependency: transitive dependency: transitive
description: description:
@@ -1038,5 +1038,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"
+6 -2
View File
@@ -2,7 +2,7 @@ name: test1
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' # This prevents publishing to pub.dev publish_to: 'none' # This prevents publishing to pub.dev
version: 1.0.2+1 version: 1.1.0+4
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
@@ -35,7 +35,9 @@ dependencies:
image_picker: ^1.0.4 image_picker: ^1.0.4
wakelock_plus: ^1.2.11 wakelock_plus: ^1.2.11
url_launcher: ^6.1.0 url_launcher: ^6.1.0
upgrader: ^12.3.0 flutter_local_notifications: ^18.0.1
timezone: ^0.9.4
flutter_timezone: ^3.0.0
dependency_overrides: dependency_overrides:
http: ^0.13.6 http: ^0.13.6
@@ -58,5 +60,7 @@ flutter:
- assets/logo.png - assets/logo.png
- assets/logo.svg - assets/logo.svg
- assets/target.png - assets/target.png
- assets/target.svg
- assets/google_logo.png - assets/google_logo.png
- assets/best_float32_nms.tflite - assets/best_float32_nms.tflite
- assets/targets/