Compare commits
10 Commits
7113b3a7fc
...
cd3398df99
| Author | SHA1 | Date | |
|---|---|---|---|
| cd3398df99 | |||
| c58ca5cf15 | |||
| d253a9c4a3 | |||
| 2ea44fab64 | |||
| f3dc1e7654 | |||
| b727e65d14 | |||
| e111f0a99b | |||
| 99b26893e7 | |||
| 3d033817a5 | |||
| 5ae91d70e6 |
@@ -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
|
|
||||||
@@ -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 = "../.."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
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>
|
||||||
@@ -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")
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 384 KiB |
|
After Width: | Height: | Size: 337 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 387 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 315 KiB |
|
After Width: | Height: | Size: 425 KiB |
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 348 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 489 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 359 KiB |
|
After Width: | Height: | Size: 317 KiB |
@@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 +360,24 @@ 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,
|
||||||
style: TextStyle(
|
children: [
|
||||||
color: isTournament ? _bulletColor : Colors.white70,
|
Icon(
|
||||||
fontWeight: FontWeight.w500,
|
Icons.emoji_events,
|
||||||
fontSize: 16,
|
size: 16,
|
||||||
),
|
color: isTournament ? _bulletColor : Colors.white70,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
isTournament ? _t('Tournament Mode') : _t('Mark as Tournament'),
|
||||||
|
style: TextStyle(
|
||||||
|
color: isTournament ? _bulletColor : Colors.white70,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
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: 8),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Row(
|
||||||
_folderTypes[folderName] == true
|
children: [
|
||||||
? '🏆 $folderName'
|
if (_folderTypes[folderName] == true) ...[
|
||||||
: folderName,
|
const Icon(Icons.emoji_events, size: 18, color: Colors.white),
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _bulletColor),
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
folderName,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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,13 +895,28 @@ Widget build(BuildContext context) {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
_folderFilter == 'all'
|
mainAxisSize: MainAxisSize.min,
|
||||||
? _t('Showing: 📂 All folders')
|
children: [
|
||||||
: _folderFilter == 'tournament'
|
Icon(
|
||||||
? _t('Showing: 🏆 Tournament folders')
|
_folderFilter == 'all'
|
||||||
: _t('Showing: 🎯 Training folders'),
|
? Icons.folder
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black),
|
: _folderFilter == 'tournament'
|
||||||
|
? Icons.emoji_events
|
||||||
|
: Icons.track_changes,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
_folderFilter == 'all'
|
||||||
|
? _t('Showing: All folders')
|
||||||
|
: _folderFilter == 'tournament'
|
||||||
|
? _t('Showing: Tournament folders')
|
||||||
|
: _t('Showing: Training folders'),
|
||||||
|
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,16 +1004,27 @@ 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,
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
style: TextStyle(
|
Icon(
|
||||||
color: _sortMode == 'date' ? Colors.white : Colors.white70,
|
Icons.calendar_today,
|
||||||
fontWeight: FontWeight.w500,
|
size: 14,
|
||||||
fontSize: 14,
|
color: _sortMode == 'date' ? Colors.white : Colors.white70,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
const SizedBox(width: 4),
|
||||||
overflow: TextOverflow.ellipsis,
|
Text(
|
||||||
|
_t('Sort by Date'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: _sortMode == 'date' ? Colors.white : Colors.white70,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1015,16 +1044,27 @@ 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,
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
style: TextStyle(
|
Icon(
|
||||||
color: _sortMode == 'score' ? Colors.white : Colors.white70,
|
Icons.emoji_events,
|
||||||
fontWeight: FontWeight.w500,
|
size: 14,
|
||||||
fontSize: 14,
|
color: _sortMode == 'score' ? Colors.white : Colors.white70,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
const SizedBox(width: 4),
|
||||||
overflow: TextOverflow.ellipsis,
|
Text(
|
||||||
|
_t('Sort by Score'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: _sortMode == 'score' ? Colors.white : Colors.white70,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
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,13 +1102,24 @@ Widget _buildStyledFilterButton(String value, String label, BuildContext context
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Row(
|
||||||
label,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: TextStyle(
|
children: [
|
||||||
color: isSelected ? Colors.white : Colors.white70,
|
Icon(
|
||||||
fontSize: 16,
|
icon,
|
||||||
fontWeight: FontWeight.w500,
|
size: 18,
|
||||||
),
|
color: isSelected ? Colors.white : Colors.white70,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected ? Colors.white : Colors.white70,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,22 +1003,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(
|
||||||
_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(),
|
||||||
@@ -1005,7 +1036,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
eventType == EventType.competition
|
eventType == EventType.competition
|
||||||
? _t('TEKMOVANJE')
|
? _t('TEKMOWANIE')
|
||||||
: eventType == EventType.sestanek
|
: eventType == EventType.sestanek
|
||||||
? _t('SESTANEK')
|
? _t('SESTANEK')
|
||||||
: _t('DEŽURSTVO'),
|
: _t('DEŽURSTVO'),
|
||||||
@@ -1052,13 +1083,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,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1068,6 +1092,147 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
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('TEKMOVANJE')
|
||||||
|
: 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 _buildEventCard(CalendarEvent event, Color accentColor) {
|
Widget _buildEventCard(CalendarEvent event, Color accentColor) {
|
||||||
final startTime = event.startTime;
|
final startTime = event.startTime;
|
||||||
final isAllDay = event.isAllDay;
|
final isAllDay = event.isAllDay;
|
||||||
@@ -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,19 +1296,19 @@ 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(
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
children: [
|
child: Padding(
|
||||||
const Icon(Icons.error, color: Colors.red, size: 100),
|
padding: const EdgeInsets.all(24),
|
||||||
const SizedBox(height: 20),
|
child: Container(
|
||||||
Text(
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||||
_t("Unable to load camera stream"),
|
decoration: BoxDecoration(
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
color: Colors.grey[900],
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error, color: _bulletColor, size: 100),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
_t("Unable to load camera stream"),
|
||||||
|
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: 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _loadStreamUrl,
|
|
||||||
child: Text(_t("Retry")),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,68 +76,73 @@ 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(
|
Expanded(
|
||||||
child: GridView.builder(
|
child: Column(
|
||||||
itemCount: 6,
|
children: [
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
// Camera grid (6 cameras)
|
||||||
crossAxisCount: 2,
|
Expanded(
|
||||||
mainAxisSpacing: 16,
|
child: GridView.builder(
|
||||||
crossAxisSpacing: 16,
|
itemCount: 6,
|
||||||
childAspectRatio: 1, // square-ish buttons
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
),
|
crossAxisCount: 2,
|
||||||
itemBuilder: (context, index) {
|
mainAxisSpacing: 16,
|
||||||
final isEnabled = _cameraButtonsEnabled[index];
|
crossAxisSpacing: 16,
|
||||||
return InkWell(
|
childAspectRatio: 1,
|
||||||
onTap: isEnabled
|
|
||||||
? () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
CameraStreamScreen(cameraIndex: index + 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isEnabled ? Colors.grey[900] : Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(16),
|
itemBuilder: (context, index) {
|
||||||
child: Column(
|
final isEnabled = _cameraButtonsEnabled[index];
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
return GestureDetector(
|
||||||
children: [
|
onTap: isEnabled
|
||||||
Icon(
|
? () {
|
||||||
Icons.videocam_rounded,
|
Navigator.push(
|
||||||
color: isEnabled ? Colors.white : Colors.grey[500],
|
context,
|
||||||
size: 36,
|
MaterialPageRoute(
|
||||||
),
|
builder: (context) =>
|
||||||
const SizedBox(height: 12),
|
CameraStreamScreen(cameraIndex: index + 1),
|
||||||
Text(
|
),
|
||||||
"${_t('Camera')} ${index + 1}",
|
);
|
||||||
textAlign: TextAlign.center,
|
}
|
||||||
style: TextStyle(
|
: null,
|
||||||
color:
|
child: Container(
|
||||||
isEnabled ? Colors.white : Colors.grey[600],
|
decoration: BoxDecoration(
|
||||||
fontSize: 20,
|
color: isEnabled ? Colors.grey[900] : Colors.grey[300],
|
||||||
fontWeight: FontWeight.w600,
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.videocam_rounded,
|
||||||
|
color: isEnabled ? Colors.white : Colors.grey[500],
|
||||||
|
size: 36,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"${_t('Camera')} ${index + 1}",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isEnabled ? Colors.white : Colors.grey[600],
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||