Compare commits
10 Commits
7113b3a7fc
...
main
| 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 {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
@@ -22,7 +23,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.shooter_app"
|
||||
minSdk = 26 // ✅ From earlier fix
|
||||
minSdk = 26
|
||||
targetSdk = 33
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
@@ -31,12 +32,18 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
@@ -16,3 +16,13 @@
|
||||
|
||||
# Keep your YOLO model loader class if needed
|
||||
-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.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
|
||||
<application
|
||||
@@ -41,6 +42,15 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<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 {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
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")
|
||||
|
||||
|
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/services.dart'; // Import for SystemChrome
|
||||
import 'screens/home_screen.dart';
|
||||
import 'services/notification_service.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
try {
|
||||
await NotificationService().initialize();
|
||||
} catch (_) {}
|
||||
|
||||
// Lock orientation to portrait only
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
@@ -46,6 +50,16 @@ class MyApp extends StatelessWidget {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart'; // For getApplicationDocument
|
||||
import 'target_view.dart'; // Import the TargetViewScreen
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../utils/translations.dart';
|
||||
import '../utils/target_colors.dart';
|
||||
|
||||
class BookScreen extends StatefulWidget {
|
||||
const BookScreen({super.key});
|
||||
@@ -25,13 +26,17 @@ class _BookScreenState extends State<BookScreen> with WidgetsBindingObserver {
|
||||
String _sortMode = 'date'; // or 'score'
|
||||
String _selectedLanguage = 'English';
|
||||
String? _expandedFolder; // Track which folder is expanded
|
||||
late Color _backgroundColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this); // Register to listen for lifecycle events
|
||||
// Initialize with default target background color
|
||||
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
|
||||
_loadBulletColor();
|
||||
_loadLanguage();
|
||||
_loadSelectedTargetColor();
|
||||
_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
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this); // Unregister from lifecycle events when disposed
|
||||
@@ -214,7 +227,7 @@ Color _getColorFromName(String colorName) {
|
||||
await file.delete();
|
||||
_loadTargetDataFiles(); // Refresh after deleting the file
|
||||
} 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 {
|
||||
final targetFiles = _groupedFiles[folderName] ?? [];
|
||||
|
||||
List<Map<String, dynamic>> allBullets = [];
|
||||
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
|
||||
// Navigate to view screen with all individual files
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TargetViewScreen(
|
||||
jsonFiles: [combinedFile], // treat it like a normal file
|
||||
jsonFiles: targetFiles, // Pass all individual files
|
||||
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(
|
||||
width: double.infinity,
|
||||
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),
|
||||
border: Border.all(
|
||||
color: isTournament ? _bulletColor : Colors.grey[700]!,
|
||||
@@ -376,14 +360,25 @@ Future<void> _renameFolder(String oldFolderName) async {
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Center(
|
||||
child: Text(
|
||||
isTournament ? _t('Tournament Mode 🏆') : _t('Mark as Tournament 🏆'),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.emoji_events,
|
||||
size: 16,
|
||||
color: isTournament ? _bulletColor : Colors.white70,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isTournament ? _t('Tournament Mode') : _t('Mark as Tournament'),
|
||||
style: TextStyle(
|
||||
color: isTournament ? _bulletColor : Colors.white70,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -445,9 +440,7 @@ Future<void> _renameFolder(String oldFolderName) async {
|
||||
|
||||
if (targetFolderName != oldFolderName) {
|
||||
if (await newFolderPath.exists()) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('A folder named "$targetFolderName" already exists.')),
|
||||
);
|
||||
debugPrint('A folder named "$targetFolderName" already exists.');
|
||||
return;
|
||||
} else {
|
||||
await oldFolderPath.rename(newFolderPath.path);
|
||||
@@ -498,16 +491,8 @@ Future<void> _renameFolder(String oldFolderName) async {
|
||||
}
|
||||
|
||||
_loadTargetDataFiles(); // Refresh file/folder list
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Folder "$folderName" deleted successfully.'),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error deleting folder: $e")),
|
||||
);
|
||||
debugPrint("Error deleting folder: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -594,14 +579,14 @@ Widget _buildFolderRow(String folderName) {
|
||||
: 'Unknown Date';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header section: Name and Date
|
||||
// Header section with grey background like toolbars
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -613,40 +598,56 @@ Widget _buildFolderRow(String folderName) {
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Small colored indicator
|
||||
// White indicator line
|
||||
Container(
|
||||
width: 4,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: _bulletColor,
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Row(
|
||||
children: [
|
||||
if (_folderTypes[folderName] == true) ...[
|
||||
const Icon(Icons.emoji_events, size: 18, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Text(
|
||||
_folderTypes[folderName] == true
|
||||
? '🏆 $folderName'
|
||||
: folderName,
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _bulletColor),
|
||||
folderName,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, size: 14, color: Colors.white),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"📅 $formattedDate",
|
||||
style: const TextStyle(fontSize: 13, color: Colors.black87),
|
||||
formattedDate,
|
||||
style: const TextStyle(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
_expandedFolder == folderName ? Icons.expand_less : Icons.expand_more,
|
||||
color: Colors.black87,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -698,7 +699,7 @@ Widget _buildFolderRow(String folderName) {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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),
|
||||
Text(
|
||||
"$totalScore",
|
||||
@@ -816,7 +817,7 @@ Widget _buildTargetsList(String folderName) {
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.emoji_events,
|
||||
color: Colors.amber,
|
||||
color: Colors.black87,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -868,6 +869,7 @@ Widget _buildTargetsList(String folderName) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _backgroundColor,
|
||||
body: Column(
|
||||
children: [
|
||||
// 🔽 Filter dropdown at the top (styled like tab bar)
|
||||
@@ -893,14 +895,29 @@ Widget build(BuildContext context) {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_folderFilter == 'all'
|
||||
? Icons.folder
|
||||
: _folderFilter == 'tournament'
|
||||
? Icons.emoji_events
|
||||
: Icons.track_changes,
|
||||
size: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_folderFilter == 'all'
|
||||
? _t('Showing: 📂 All folders')
|
||||
? _t('Showing: All folders')
|
||||
: _folderFilter == 'tournament'
|
||||
? _t('Showing: 🏆 Tournament folders')
|
||||
: _t('Showing: 🎯 Training folders'),
|
||||
? _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),
|
||||
],
|
||||
),
|
||||
@@ -917,6 +934,7 @@ Widget build(BuildContext context) {
|
||||
thickness: 6, // optional: adjust width
|
||||
radius: const Radius.circular(8),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
itemCount: _filteredFolderNames().length,
|
||||
itemBuilder: (context, index) {
|
||||
String folderName = _filteredFolderNames()[index];
|
||||
@@ -955,11 +973,11 @@ void _showFolderFilterPopup(BuildContext context) {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Folder type filters
|
||||
_buildStyledFilterButton('all', _t('📂 All folders'), context),
|
||||
_buildStyledFilterButton('all', Icons.folder, _t('All folders'), context),
|
||||
const SizedBox(height: 12),
|
||||
_buildStyledFilterButton('tournament', _t('🏆 Tournament'), context),
|
||||
_buildStyledFilterButton('tournament', Icons.emoji_events, _t('Tournament'), context),
|
||||
const SizedBox(height: 12),
|
||||
_buildStyledFilterButton('training', _t('🎯 Training'), context),
|
||||
_buildStyledFilterButton('training', Icons.track_changes, _t('Training'), context),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -986,8 +1004,17 @@ void _showFolderFilterPopup(BuildContext context) {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_t('📅 Sort by Date'),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: _sortMode == 'date' ? Colors.white : Colors.white70,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_t('Sort by Date'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: _sortMode == 'date' ? Colors.white : Colors.white70,
|
||||
@@ -997,6 +1024,8 @@ void _showFolderFilterPopup(BuildContext context) {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1015,8 +1044,17 @@ void _showFolderFilterPopup(BuildContext context) {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_t('🏆 Sort by Score'),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.emoji_events,
|
||||
size: 14,
|
||||
color: _sortMode == 'score' ? Colors.white : Colors.white70,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_t('Sort by Score'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: _sortMode == 'score' ? Colors.white : Colors.white70,
|
||||
@@ -1026,6 +1064,8 @@ void _showFolderFilterPopup(BuildContext context) {
|
||||
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;
|
||||
|
||||
return SizedBox(
|
||||
@@ -1062,7 +1102,16 @@ Widget _buildStyledFilterButton(String value, String label, BuildContext context
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: isSelected ? Colors.white : Colors.white70,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : Colors.white70,
|
||||
@@ -1070,6 +1119,8 @@ Widget _buildStyledFilterButton(String value, String label, BuildContext context
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,7 +5,9 @@ import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../services/google_calendar_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../utils/translations.dart';
|
||||
import '../utils/target_colors.dart';
|
||||
|
||||
class Contact {
|
||||
final String firstName;
|
||||
@@ -45,6 +47,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
String _selectedLanguage = 'English';
|
||||
bool _showContacts = false;
|
||||
bool _showDebugPreview = false;
|
||||
late Color _backgroundColor;
|
||||
|
||||
// Members contact list
|
||||
final List<Contact> _contacts = [
|
||||
@@ -69,8 +72,11 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeDateFormatting('sl_SI', null); // Initialize Slovenian locale
|
||||
// Initialize with default target background color
|
||||
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
|
||||
_loadBulletColor();
|
||||
_loadLanguage();
|
||||
_loadSelectedTargetColor();
|
||||
_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() {
|
||||
setState(() {
|
||||
_showDebugPreview = !_showDebugPreview;
|
||||
@@ -221,6 +235,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
_events = filteredEvents;
|
||||
});
|
||||
}
|
||||
NotificationService().scheduleEventNotifications(filteredEvents);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -473,15 +488,15 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
onTap: () => _makePhoneCall(contact.phone),
|
||||
onLongPress: () => _showContactPopup(context, contact),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.08),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -490,37 +505,37 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Colored header bar (like event cards)
|
||||
// Header bar with bullet color and transparency
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withValues(alpha: 0.2),
|
||||
color: accentColor.withValues(alpha: 0.6),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Small colored indicator
|
||||
// White indicator line
|
||||
Container(
|
||||
width: 3,
|
||||
height: 12,
|
||||
width: 4,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor,
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 7),
|
||||
// Contact name
|
||||
const SizedBox(width: 12),
|
||||
// Contact name in white
|
||||
Expanded(
|
||||
child: Text(
|
||||
_capitalize(contact.fullName),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: accentColor,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -670,6 +685,22 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_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),
|
||||
],
|
||||
),
|
||||
@@ -717,7 +748,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: _backgroundColor,
|
||||
body: Column(
|
||||
children: [
|
||||
// Tab buttons
|
||||
@@ -928,7 +959,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
time = '18:00 - 19:00';
|
||||
break;
|
||||
case EventType.regular:
|
||||
cardAccentColor = Colors.blue;
|
||||
cardAccentColor = accentColor;
|
||||
eventIcon = Icons.event;
|
||||
title = _t('Duty Shift');
|
||||
time = '14:00 - 16:00';
|
||||
@@ -942,11 +973,11 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
// Card glow effect for today's events
|
||||
// Strong glow effect for today's events (sample is always today)
|
||||
BoxShadow(
|
||||
color: cardAccentColor.withValues(alpha: 0.3),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
color: cardAccentColor.withValues(alpha: 0.4),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
// Subtle inner shadow
|
||||
BoxShadow(
|
||||
@@ -964,7 +995,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: cardAccentColor.withValues(alpha: 0.2),
|
||||
color: cardAccentColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
@@ -972,26 +1003,167 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Small colored indicator
|
||||
// Small white indicator line
|
||||
Container(
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: cardAccentColor,
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_t('Today'),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cardAccentColor,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Event type badge - filled badges for all types
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: cardAccentColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(eventIcon, size: 12, color: Colors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
eventType == EventType.competition
|
||||
? _t('TEKMOWANIE')
|
||||
: eventType == EventType.sestanek
|
||||
? _t('SESTANEK')
|
||||
: _t('DEŽURSTVO'),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Main content
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title with icon
|
||||
Row(
|
||||
children: [
|
||||
Icon(eventIcon, size: 20, color: cardAccentColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Time range
|
||||
_buildDetailRowInline(
|
||||
icon: Icons.access_time,
|
||||
value: time,
|
||||
accentColor: cardAccentColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOtherDateEventCard(Color accentColor, EventType eventType, String dateLabel) {
|
||||
// Color coding for each event type
|
||||
Color cardAccentColor;
|
||||
Color backgroundColor = Colors.grey[300]!;
|
||||
IconData eventIcon;
|
||||
String title;
|
||||
String time;
|
||||
|
||||
switch (eventType) {
|
||||
case EventType.competition:
|
||||
cardAccentColor = Colors.orange;
|
||||
eventIcon = Icons.emoji_events;
|
||||
title = _t('League / Competition');
|
||||
time = '09:00 - 18:00';
|
||||
break;
|
||||
case EventType.sestanek:
|
||||
cardAccentColor = Colors.red;
|
||||
eventIcon = Icons.groups;
|
||||
title = _t('Team Meeting');
|
||||
time = '18:00 - 19:00';
|
||||
break;
|
||||
case EventType.regular:
|
||||
cardAccentColor = accentColor;
|
||||
eventIcon = Icons.event;
|
||||
title = _t('Duty Shift');
|
||||
time = '14:00 - 16:00';
|
||||
break;
|
||||
}
|
||||
|
||||
return RepaintBoundary(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Date header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: cardAccentColor.withValues(alpha: 0.7),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Small white indicator line
|
||||
Container(
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
dateLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Event type badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
@@ -1052,13 +1224,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
value: time,
|
||||
accentColor: cardAccentColor,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// Location
|
||||
_buildDetailRowInline(
|
||||
icon: Icons.location_on,
|
||||
value: _t('Strelišče Ljubljana'),
|
||||
accentColor: cardAccentColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1090,7 +1255,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
eventIcon = Icons.groups; // People icon for meetings
|
||||
break;
|
||||
case EventType.regular:
|
||||
cardAccentColor = Colors.blue;
|
||||
cardAccentColor = accentColor;
|
||||
backgroundColor = Colors.grey[300]!;
|
||||
eventIcon = Icons.event;
|
||||
break;
|
||||
@@ -1131,20 +1296,20 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
// Card glow effect (stronger for today's events)
|
||||
if (isToday)
|
||||
// Strong glow effect only for today's events
|
||||
if (isToday) ...[
|
||||
BoxShadow(
|
||||
color: cardAccentColor.withValues(alpha: 0.3),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
color: cardAccentColor.withValues(alpha: 0.4),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
// Subtle inner shadow
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1154,9 +1319,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isToday
|
||||
? cardAccentColor.withValues(alpha: 0.2)
|
||||
: cardAccentColor.withValues(alpha: 0.1),
|
||||
color: isToday ? cardAccentColor : cardAccentColor.withValues(alpha: 0.7),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
@@ -1164,22 +1327,22 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Small colored indicator
|
||||
// Small white indicator line
|
||||
Container(
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: cardAccentColor,
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
dateText,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cardAccentColor,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'yolo_detector.dart';
|
||||
import 'preview_screen.dart';
|
||||
import '../utils/translations.dart';
|
||||
import '../utils/target_colors.dart';
|
||||
|
||||
class CameraScreen extends StatefulWidget {
|
||||
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)
|
||||
int _numberOfBullets = 5;
|
||||
String _selectedLanguage = 'English';
|
||||
late Color _backgroundColor;
|
||||
|
||||
// Brightness and contrast controls
|
||||
double _brightness = 0.0; // -1.0 to 1.0
|
||||
@@ -31,10 +33,13 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize with default target background color
|
||||
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
|
||||
_loadBulletCount();
|
||||
_initCamera();
|
||||
_loadBulletColor(); // 👈
|
||||
_loadLanguage();
|
||||
_loadSelectedTargetColor();
|
||||
_loadBrightnessContrast();
|
||||
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 {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final selected = prefs.getString('bulletColor') ?? 'Blue';
|
||||
@@ -445,6 +458,7 @@ Color _getColorFromName(String colorName) {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _backgroundColor,
|
||||
extendBody: true,
|
||||
appBar: AppBar(
|
||||
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 'login_screen.dart';
|
||||
import 'camera_screen.dart';
|
||||
import 'spectator_screen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../utils/translations.dart';
|
||||
|
||||
import '../utils/target_colors.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -20,6 +21,8 @@ class HomeScreen extends StatefulWidget {
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int _selectedIndex = 1; // Default to Shooting Screen
|
||||
String _selectedLanguage = 'English';
|
||||
String _selectedTarget = 'assets/target.png';
|
||||
late Color _backgroundColor;
|
||||
|
||||
static final List<Widget> _pages = <Widget>[
|
||||
BookScreen(),
|
||||
@@ -40,7 +43,10 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize with default target background color
|
||||
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
|
||||
_loadLanguage();
|
||||
_loadSelectedTarget();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (index == 2) {
|
||||
_openCameraScan();
|
||||
@@ -99,11 +115,25 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
automaticallyImplyLeading: false,
|
||||
scrolledUnderElevation: 0, // For Material 3 (Flutter 3.7+)
|
||||
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: const EdgeInsets.only(right: 16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'V1.0.2',
|
||||
'V1.1',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[400],
|
||||
fontSize: 12,
|
||||
@@ -114,6 +144,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: _backgroundColor,
|
||||
body: _pages[_selectedIndex],
|
||||
bottomNavigationBar: BottomNavBar(
|
||||
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(
|
||||
colorFilter: _buildColorFilter(),
|
||||
child: Mjpeg(
|
||||
key: ValueKey(_streamUrl),
|
||||
stream: _streamUrl!,
|
||||
isLive: true,
|
||||
fit: BoxFit.contain,
|
||||
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() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[900],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error, color: Colors.red, size: 100),
|
||||
const SizedBox(height: 20),
|
||||
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),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_t("Please ensure:"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.wifi, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_t("WiFi is enabled"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: _loadStreamUrl,
|
||||
child: Text(_t("Retry")),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.signal_cellular_off, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_t("Mobile data is disabled"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _loadStreamUrl,
|
||||
icon: const Icon(Icons.refresh, size: 20),
|
||||
label: Text(_t("Reload")),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _bulletColor,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'stream_screen.dart'; // Import the camera stream screen
|
||||
import '../utils/translations.dart';
|
||||
import '../utils/target_colors.dart';
|
||||
|
||||
class TargetScreen extends StatefulWidget {
|
||||
const TargetScreen({super.key});
|
||||
@@ -14,11 +15,15 @@ class _TargetScreenState extends State<TargetScreen> {
|
||||
List<bool> _cameraButtonsEnabled =
|
||||
List.generate(6, (_) => false); // Initially, all buttons are disabled
|
||||
String _selectedLanguage = 'English';
|
||||
late Color _backgroundColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize with default target background color
|
||||
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
|
||||
_loadLanguage();
|
||||
_loadSelectedTargetColor();
|
||||
_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
|
||||
Future<void> _loadCameraData() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -63,14 +76,18 @@ class _TargetScreenState extends State<TargetScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _backgroundColor,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
// Camera grid (6 cameras)
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
itemCount: 6,
|
||||
@@ -78,11 +95,11 @@ class _TargetScreenState extends State<TargetScreen> {
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
childAspectRatio: 1, // square-ish buttons
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isEnabled = _cameraButtonsEnabled[index];
|
||||
return InkWell(
|
||||
return GestureDetector(
|
||||
onTap: isEnabled
|
||||
? () {
|
||||
Navigator.push(
|
||||
@@ -94,7 +111,6 @@ class _TargetScreenState extends State<TargetScreen> {
|
||||
);
|
||||
}
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isEnabled ? Colors.grey[900] : Colors.grey[300],
|
||||
@@ -114,8 +130,7 @@ class _TargetScreenState extends State<TargetScreen> {
|
||||
"${_t('Camera')} ${index + 1}",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isEnabled ? Colors.white : Colors.grey[600],
|
||||
color: isEnabled ? Colors.white : Colors.grey[600],
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -130,6 +145,9 @@ class _TargetScreenState extends State<TargetScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:convert';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../utils/translations.dart';
|
||||
import '../utils/target_colors.dart';
|
||||
|
||||
class TargetViewScreen extends StatefulWidget {
|
||||
final List<File> jsonFiles;
|
||||
@@ -26,6 +27,7 @@ class TargetViewScreen extends StatefulWidget {
|
||||
class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
int _currentIndex = 0;
|
||||
List<Offset> _bullets = [];
|
||||
List<Offset> _scaledBullets = []; // Bullets scaled to display target size
|
||||
int _totalScore = 0;
|
||||
Offset? _targetCenter;
|
||||
Uint8List? _targetImageBytes;
|
||||
@@ -34,20 +36,30 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
int? _highlightedBulletIndex;
|
||||
int? _pressedScoreGroup;
|
||||
late String _targetFilePath;
|
||||
|
||||
final String targetAssetPath = 'assets/target.png';
|
||||
final double bulletSize = 19;
|
||||
late String targetAssetPath;
|
||||
late String _selectedTargetPath; // For display
|
||||
double _scaleFactorX = 1.0; // Scale factor for X coordinate
|
||||
double _scaleFactorY = 1.0; // Scale factor for Y coordinate
|
||||
|
||||
Color _bulletColor = Colors.blue; // default/fallback bullet color
|
||||
String _selectedLanguage = 'English';
|
||||
|
||||
Color _backgroundColor = Colors.white;
|
||||
bool _showChartView = false; // Toggle between bullet view and chart view
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadBulletColor();
|
||||
_loadLanguage();
|
||||
_loadJsonAndImage();
|
||||
targetAssetPath = 'assets/target.png'; // Default
|
||||
_initializeScreen();
|
||||
}
|
||||
|
||||
Future<void> _initializeScreen() async {
|
||||
await Future.wait([
|
||||
_loadBulletColor(),
|
||||
_loadLanguage(),
|
||||
_loadSelectedTarget(),
|
||||
]);
|
||||
await _loadJsonAndImage();
|
||||
}
|
||||
|
||||
String _t(String key) {
|
||||
@@ -61,6 +73,21 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadSelectedTarget() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final selectedTarget =
|
||||
prefs.getString('selectedTarget') ?? 'assets/target.png';
|
||||
// Always use default target for bullet positioning
|
||||
targetAssetPath = 'assets/target.png';
|
||||
// Store selected target for display
|
||||
_selectedTargetPath = selectedTarget;
|
||||
_backgroundColor = _getBackgroundColorForTarget(selectedTarget);
|
||||
}
|
||||
|
||||
Color _getBackgroundColorForTarget(String targetPath) {
|
||||
return TargetColors.getBackgroundColor(targetPath);
|
||||
}
|
||||
|
||||
Future<void> _loadBulletColor() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final selectedColor = prefs.getString('bulletColor') ?? 'Blue';
|
||||
@@ -107,25 +134,81 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future<void> _loadJsonAndImage() async {
|
||||
try {
|
||||
final byteData = await rootBundle.load(targetAssetPath);
|
||||
final bytes = byteData.buffer.asUint8List();
|
||||
_targetImageDecoded = img.decodeImage(bytes);
|
||||
// Load default target for bullet positioning reference
|
||||
final defaultByteData = await rootBundle.load(targetAssetPath);
|
||||
final defaultBytes = defaultByteData.buffer.asUint8List();
|
||||
img.Image? defaultTargetDecoded = img.decodeImage(defaultBytes);
|
||||
|
||||
if (_targetImageDecoded == null) {
|
||||
print("Failed to decode image.");
|
||||
if (defaultTargetDecoded == null) {
|
||||
print("Failed to decode default target image.");
|
||||
return;
|
||||
}
|
||||
|
||||
_targetImageBytes = Uint8List.fromList(img.encodePng(_targetImageDecoded!));
|
||||
// Calculate target center from default target (for bullet positioning)
|
||||
_targetCenter = Offset(
|
||||
_targetImageDecoded!.width / 2,
|
||||
_targetImageDecoded!.height / 2 - 1,
|
||||
defaultTargetDecoded.width / 2,
|
||||
defaultTargetDecoded.height / 2 - 1,
|
||||
);
|
||||
|
||||
// Load selected target for display
|
||||
final displayByteData = await rootBundle.load(_selectedTargetPath);
|
||||
final displayBytes = displayByteData.buffer.asUint8List();
|
||||
_targetImageDecoded = img.decodeImage(displayBytes);
|
||||
|
||||
if (_targetImageDecoded == null) {
|
||||
print("Failed to decode display target image.");
|
||||
return;
|
||||
}
|
||||
|
||||
_targetImageBytes =
|
||||
Uint8List.fromList(img.encodePng(_targetImageDecoded!));
|
||||
|
||||
if (widget.isCombinedView) {
|
||||
// In combined view, collect all bullets from all files
|
||||
final allBullets = <Offset>[];
|
||||
String latestDate = 'Unknown Date';
|
||||
|
||||
for (var file in widget.jsonFiles) {
|
||||
final jsonString = await file.readAsString();
|
||||
final data = jsonDecode(jsonString);
|
||||
|
||||
final bulletsJson = data['bullets'] as List?;
|
||||
if (bulletsJson != null) {
|
||||
allBullets.addAll(
|
||||
bulletsJson.map((b) => Offset(b['x'].toDouble(), b['y'].toDouble())).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
if (data['date'] != null) {
|
||||
latestDate = data['date'];
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and convert date format from yyyy-m-d to dd/MM/yyyy
|
||||
String rawDate = latestDate;
|
||||
if (rawDate != 'Unknown Date' && rawDate.contains('-')) {
|
||||
try {
|
||||
final parts = rawDate.split('-');
|
||||
if (parts.length == 3) {
|
||||
final year = parts[0];
|
||||
final month = parts[1].padLeft(2, '0');
|
||||
final day = parts[2].padLeft(2, '0');
|
||||
_date = '$day/$month/$year';
|
||||
} else {
|
||||
_date = rawDate;
|
||||
}
|
||||
} catch (e) {
|
||||
_date = rawDate;
|
||||
}
|
||||
} else {
|
||||
_date = rawDate;
|
||||
}
|
||||
|
||||
_bullets = allBullets;
|
||||
} else {
|
||||
// Single file view - load from current file
|
||||
final currentFile = widget.jsonFiles[_currentIndex];
|
||||
final jsonString = await currentFile.readAsString();
|
||||
final data = jsonDecode(jsonString);
|
||||
@@ -150,9 +233,24 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
_date = rawDate;
|
||||
}
|
||||
final bulletsJson = data['bullets'] as List;
|
||||
_bullets = bulletsJson.map((b) => Offset(b['x'], b['y'])).toList();
|
||||
_bullets = bulletsJson.map((b) => Offset(b['x'].toDouble(), b['y'].toDouble())).toList();
|
||||
}
|
||||
|
||||
_targetFilePath = currentFile.path;
|
||||
_targetFilePath = widget.isCombinedView
|
||||
? widget.jsonFiles.first.path
|
||||
: widget.jsonFiles[_currentIndex].path;
|
||||
|
||||
// Calculate scale factors based on image dimensions
|
||||
_scaleFactorX = _targetImageDecoded!.width / defaultTargetDecoded.width;
|
||||
_scaleFactorY = _targetImageDecoded!.height / defaultTargetDecoded.height;
|
||||
|
||||
// Scale bullets to match display target size
|
||||
_scaledBullets = _bullets.map((bullet) {
|
||||
return Offset(
|
||||
bullet.dx * _scaleFactorX,
|
||||
bullet.dy * _scaleFactorY,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
_totalScore = _bullets
|
||||
.map((b) => _scoreForBullet(b, _targetCenter!))
|
||||
@@ -194,9 +292,16 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: _backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('📅 $_date', style: const TextStyle(color: Colors.white)),
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, size: 20, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text(_date, style: const TextStyle(color: Colors.white)),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.grey[900],
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
@@ -210,7 +315,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.grey[900],
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
@@ -229,7 +335,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_t("Are you sure you want to delete this target?"),
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 16),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -245,7 +352,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.redAccent,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -263,7 +371,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[800],
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -277,13 +386,14 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _targetImageDecoded == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _showChartView
|
||||
? _buildChartView()
|
||||
: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -313,6 +423,10 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
|
||||
final scaleX = displayWidth / imgWidth;
|
||||
final scaleY = displayHeight / imgHeight;
|
||||
// Scale bullet size by target scale factors and display scale factors
|
||||
final dynamicBulletSize =
|
||||
(((_scaleFactorX + _scaleFactorY) / 2) * 53) *
|
||||
((scaleX + scaleY) / 2);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
@@ -321,36 +435,42 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
top: offsetY,
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
child: Image.memory(_targetImageBytes!, fit: BoxFit.fill),
|
||||
child: Image.memory(_targetImageBytes!,
|
||||
fit: BoxFit.fill),
|
||||
),
|
||||
..._bullets.asMap().entries.map((entry) {
|
||||
..._scaledBullets.asMap().entries.map((entry) {
|
||||
final i = entry.key;
|
||||
final bullet = entry.value;
|
||||
final dx = bullet.dx * scaleX + offsetX;
|
||||
final dy = bullet.dy * scaleY + offsetY;
|
||||
final score = _scoreForBullet(bullet, _targetCenter ?? Offset.zero);
|
||||
final scaledBullet = entry.value;
|
||||
// Scaled bullets are already scaled to display target dimensions
|
||||
final dx = scaledBullet.dx * scaleX + offsetX;
|
||||
final dy = scaledBullet.dy * scaleY + offsetY;
|
||||
// Score is calculated using original bullet position and default target center
|
||||
final originalBullet = _bullets[i];
|
||||
final score = _scoreForBullet(
|
||||
originalBullet, _targetCenter ?? Offset.zero);
|
||||
final isHighlighted = widget.isCombinedView
|
||||
? (_pressedScoreGroup == score)
|
||||
: (i == _highlightedBulletIndex);
|
||||
|
||||
return Positioned(
|
||||
left: dx - bulletSize / 2,
|
||||
top: dy - bulletSize / 2,
|
||||
left: dx - dynamicBulletSize / 2,
|
||||
top: dy - dynamicBulletSize / 2,
|
||||
child: Container(
|
||||
width: bulletSize,
|
||||
height: bulletSize,
|
||||
width: dynamicBulletSize,
|
||||
height: dynamicBulletSize,
|
||||
decoration: BoxDecoration(
|
||||
color: _bulletColor.withOpacity(0.8),
|
||||
color: _bulletColor.withValues(alpha: 0.8),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isHighlighted ? Colors.red : Colors.black,
|
||||
color: isHighlighted
|
||||
? Colors.red
|
||||
: Colors.black,
|
||||
width: isHighlighted ? 3 : 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -358,8 +478,9 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
color: _backgroundColor,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: widget.isCombinedView
|
||||
? _buildGroupedScoreButtons()
|
||||
@@ -373,35 +494,179 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
color: Colors.grey[900],
|
||||
padding: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
height: 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Total score
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.track_changes,
|
||||
size: 20, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Combined badge with text and score
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.grey[700]!,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
"🎯 ${_t('Total score')} $_totalScore",
|
||||
Text(
|
||||
_t('Total score'),
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontSize: 11,
|
||||
color: Colors.white54,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$_totalScore",
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// Chart toggle button
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showChartView = !_showChartView;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
_showChartView ? Icons.bar_chart : Icons.image,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChartView() {
|
||||
if (widget.isCombinedView) {
|
||||
// Folder view: each target position as a bar with combined scores
|
||||
return _buildFolderChartView();
|
||||
} else {
|
||||
// Single file view: each shot as a bar
|
||||
return _buildSingleFileChartView();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFolderChartView() {
|
||||
// Calculate target scores from all files
|
||||
// Each file represents one target at position (fileIndex + 1)
|
||||
final Map<int, int> targetScores = {};
|
||||
|
||||
for (int fileIdx = 0; fileIdx < widget.jsonFiles.length; fileIdx++) {
|
||||
final file = widget.jsonFiles[fileIdx];
|
||||
final targetPosition = fileIdx + 1; // T1, T2, T3, etc.
|
||||
|
||||
final data = jsonDecode(file.readAsStringSync());
|
||||
final bullets = (data['bullets'] as List?) ?? [];
|
||||
|
||||
// Sum scores for this target
|
||||
int targetScore = 0;
|
||||
for (int j = 0; j < bullets.length; j++) {
|
||||
final bullet = Offset(bullets[j]['x'].toDouble(), bullets[j]['y'].toDouble());
|
||||
final score = _scoreForBullet(bullet, _targetCenter ?? Offset.zero);
|
||||
targetScore += score;
|
||||
}
|
||||
targetScores[targetPosition] = targetScore;
|
||||
}
|
||||
|
||||
if (targetScores.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_t('No data available'),
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sortedTargets = targetScores.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
|
||||
final maxScore = sortedTargets.map((e) => e.value).reduce((a, b) => a > b ? a : b).toDouble();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
|
||||
child: Card(
|
||||
color: Colors.white,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: CustomPaint(
|
||||
painter: BarChartPainter(
|
||||
data: sortedTargets,
|
||||
maxScore: maxScore,
|
||||
bulletColor: _bulletColor,
|
||||
backgroundColor: Colors.white,
|
||||
selectedLanguage: _selectedLanguage,
|
||||
translateFunc: _t,
|
||||
),
|
||||
size: Size.infinite,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleFileChartView() {
|
||||
// Single file view: each shot as a bar
|
||||
final Map<int, int> shotScores = {};
|
||||
for (int i = 0; i < _bullets.length; i++) {
|
||||
final score = _scoreForBullet(_bullets[i], _targetCenter ?? Offset.zero);
|
||||
shotScores[i + 1] = score; // Shot number starts at 1
|
||||
}
|
||||
|
||||
if (shotScores.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_t('No data available'),
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sortedShots = shotScores.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
|
||||
final maxScore = sortedShots.map((e) => e.value).reduce((a, b) => a > b ? a : b).toDouble();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
|
||||
child: Card(
|
||||
color: Colors.white,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: CustomPaint(
|
||||
painter: BarChartPainter(
|
||||
data: sortedShots,
|
||||
maxScore: maxScore,
|
||||
bulletColor: _bulletColor,
|
||||
isShotView: true,
|
||||
backgroundColor: Colors.white,
|
||||
selectedLanguage: _selectedLanguage,
|
||||
translateFunc: _t,
|
||||
xAxisLabelKey: 'Points',
|
||||
),
|
||||
size: Size.infinite,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -470,7 +735,7 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
decoration: BoxDecoration(
|
||||
color: _pressedScoreGroup == score
|
||||
? Colors.red[400]
|
||||
: _bulletColor.withOpacity(0.85),
|
||||
: _bulletColor.withValues(alpha: 0.85),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
@@ -499,8 +764,6 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
List<Widget> _buildIndividualScoreButtons() {
|
||||
final scoredBullets = _bullets.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
@@ -510,7 +773,8 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
}).toList();
|
||||
|
||||
// Sort descending by score
|
||||
scoredBullets.sort((a, b) => (b['score'] as int).compareTo(a['score'] as int));
|
||||
scoredBullets
|
||||
.sort((a, b) => (b['score'] as int).compareTo(a['score'] as int));
|
||||
|
||||
return scoredBullets.map((entry) {
|
||||
final int index = entry['index'] as int;
|
||||
@@ -561,7 +825,7 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
decoration: BoxDecoration(
|
||||
color: _highlightedBulletIndex == index
|
||||
? Colors.red[400]
|
||||
: _bulletColor.withOpacity(0.85),
|
||||
: _bulletColor.withValues(alpha: 0.85),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
@@ -586,5 +850,239 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class BarChartPainter extends CustomPainter {
|
||||
final List<MapEntry<int, int>> data;
|
||||
final double maxScore;
|
||||
final Color bulletColor;
|
||||
final bool isShotView;
|
||||
final Color backgroundColor;
|
||||
final String selectedLanguage;
|
||||
final Function(String) translateFunc;
|
||||
final String? xAxisLabelKey;
|
||||
|
||||
BarChartPainter({
|
||||
required this.data,
|
||||
required this.maxScore,
|
||||
required this.bulletColor,
|
||||
this.isShotView = false,
|
||||
this.backgroundColor = Colors.white,
|
||||
this.selectedLanguage = 'English',
|
||||
required this.translateFunc,
|
||||
this.xAxisLabelKey,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
const leftLabelWidth = 50.0;
|
||||
const padding = 24.0;
|
||||
const bottomPadding = 35.0;
|
||||
final chartWidth = size.width - leftLabelWidth - padding;
|
||||
final chartHeight = size.height - padding - bottomPadding;
|
||||
|
||||
// Determine if background is light or dark
|
||||
final isLightBackground = _isLightColor(backgroundColor);
|
||||
final axisColor = isLightBackground ? Colors.black54 : Colors.white54;
|
||||
final gridColor = isLightBackground
|
||||
? Colors.black.withValues(alpha: 0.08)
|
||||
: Colors.white.withValues(alpha: 0.08);
|
||||
final labelColor = isLightBackground ? Colors.black87 : Colors.white70;
|
||||
|
||||
// Draw Y axis (left) - thinner
|
||||
canvas.drawLine(
|
||||
Offset(leftLabelWidth, padding),
|
||||
Offset(leftLabelWidth, size.height - bottomPadding),
|
||||
Paint()
|
||||
..color = axisColor
|
||||
..strokeWidth = 1,
|
||||
);
|
||||
|
||||
// Draw X axis (bottom) - thinner
|
||||
canvas.drawLine(
|
||||
Offset(leftLabelWidth, size.height - bottomPadding),
|
||||
Offset(size.width - padding, size.height - bottomPadding),
|
||||
Paint()
|
||||
..color = axisColor
|
||||
..strokeWidth = 1,
|
||||
);
|
||||
|
||||
// Draw Y axis label (Score)
|
||||
final yAxisLabel = TextPainter(
|
||||
text: TextSpan(
|
||||
text: translateFunc('Score'),
|
||||
style: TextStyle(color: axisColor, fontSize: 10),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
yAxisLabel.layout();
|
||||
yAxisLabel.paint(
|
||||
canvas,
|
||||
Offset(8, padding - 10),
|
||||
);
|
||||
|
||||
// Draw X axis label (Targets, Shots, or Points)
|
||||
late String xAxisLabelText;
|
||||
if (xAxisLabelKey != null) {
|
||||
xAxisLabelText = translateFunc(xAxisLabelKey!);
|
||||
} else {
|
||||
xAxisLabelText = isShotView ? translateFunc('Shots') : translateFunc('Targets');
|
||||
}
|
||||
final xAxisLabel = TextPainter(
|
||||
text: TextSpan(
|
||||
text: xAxisLabelText,
|
||||
style: TextStyle(color: axisColor, fontSize: 10),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
xAxisLabel.layout();
|
||||
xAxisLabel.paint(
|
||||
canvas,
|
||||
Offset(size.width - padding - 40, size.height - 17),
|
||||
);
|
||||
|
||||
// Draw grid lines and X axis labels (score values)
|
||||
for (int i = 0; i <= 10; i++) {
|
||||
final xValue = (maxScore / 10) * i;
|
||||
final xPos = leftLabelWidth + (chartWidth * i / 10);
|
||||
|
||||
// Grid line (lighter)
|
||||
canvas.drawLine(
|
||||
Offset(xPos, padding),
|
||||
Offset(xPos, size.height - bottomPadding),
|
||||
Paint()
|
||||
..color = gridColor
|
||||
..strokeWidth = 0.5,
|
||||
);
|
||||
|
||||
// X axis label (score) - smaller and lighter
|
||||
if (i % 2 == 0) { // Only show every 2nd label to avoid clutter
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '${xValue.toInt()}',
|
||||
style: TextStyle(color: labelColor.withValues(alpha: 0.6), fontSize: 10),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(xPos - textPainter.width / 2, size.height - bottomPadding + 8),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw horizontal bars
|
||||
if (data.isNotEmpty) {
|
||||
final barHeight = chartHeight / (data.length * 1.3);
|
||||
final spacing = barHeight * 0.3;
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
final entry = data[i];
|
||||
final targetNum = entry.key;
|
||||
final score = entry.value;
|
||||
|
||||
final barWidth = (score / maxScore) * chartWidth;
|
||||
final yPos = padding + (i * (barHeight + spacing)) + spacing;
|
||||
final xStart = leftLabelWidth;
|
||||
|
||||
// Draw bar with subtle shadow effect
|
||||
final shadowPaint = Paint()
|
||||
..color = Colors.black.withValues(alpha: 0.1)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(
|
||||
xStart + 1,
|
||||
yPos + 1,
|
||||
barWidth,
|
||||
barHeight,
|
||||
),
|
||||
const Radius.circular(6),
|
||||
),
|
||||
shadowPaint,
|
||||
);
|
||||
|
||||
// Draw main bar with rounded corners
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(
|
||||
xStart,
|
||||
yPos,
|
||||
barWidth,
|
||||
barHeight,
|
||||
),
|
||||
const Radius.circular(6),
|
||||
),
|
||||
Paint()..color = bulletColor,
|
||||
);
|
||||
|
||||
// Draw Y axis label (target name)
|
||||
final label = isShotView ? 'S$targetNum' : 'T$targetNum';
|
||||
final labelPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: TextStyle(
|
||||
color: labelColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
labelPainter.layout();
|
||||
labelPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
leftLabelWidth - labelPainter.width - 10,
|
||||
yPos + barHeight / 2 - labelPainter.height / 2,
|
||||
),
|
||||
);
|
||||
|
||||
// Draw score at end of bar (only if bar is wide enough)
|
||||
if (barWidth > 20) {
|
||||
final scorePainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '${score.toInt()}',
|
||||
style: TextStyle(
|
||||
color: isLightBackground ? Colors.black87 : Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
scorePainter.layout();
|
||||
scorePainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
xStart + barWidth + 8,
|
||||
yPos + barHeight / 2 - scorePainter.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(BarChartPainter oldDelegate) {
|
||||
return oldDelegate.data != data ||
|
||||
oldDelegate.maxScore != maxScore ||
|
||||
oldDelegate.bulletColor != bulletColor ||
|
||||
oldDelegate.backgroundColor != backgroundColor ||
|
||||
oldDelegate.isShotView != isShotView ||
|
||||
oldDelegate.selectedLanguage != selectedLanguage ||
|
||||
oldDelegate.xAxisLabelKey != xAxisLabelKey;
|
||||
}
|
||||
|
||||
bool _isLightColor(Color color) {
|
||||
// Calculate luminance to determine if background is light or dark
|
||||
final r = (color.r * 255.0).round();
|
||||
final g = (color.g * 255.0).round();
|
||||
final b = (color.b * 255.0).round();
|
||||
final luminance = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return luminance > 128;
|
||||
}
|
||||
}
|
||||
@@ -83,11 +83,21 @@ class YOLODetector {
|
||||
|
||||
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(
|
||||
x1 * croppedResult.cropWidth + croppedResult.offsetX,
|
||||
y1 * croppedResult.cropHeight + croppedResult.offsetY,
|
||||
x2 * croppedResult.cropWidth + croppedResult.offsetX,
|
||||
y2 * croppedResult.cropHeight + croppedResult.offsetY,
|
||||
scaledX1 + croppedResult.offsetX,
|
||||
scaledY1 + croppedResult.offsetY,
|
||||
scaledX2 + croppedResult.offsetX,
|
||||
scaledY2 + croppedResult.offsetY,
|
||||
score,
|
||||
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',
|
||||
},
|
||||
|
||||
// Appearance Settings
|
||||
'Appearance': {
|
||||
'English': 'Appearance',
|
||||
'Slovenian': 'Videz',
|
||||
},
|
||||
'Appearance Settings': {
|
||||
'English': 'Appearance Settings',
|
||||
'Slovenian': 'Nastavitve videza',
|
||||
},
|
||||
'Bullet': {
|
||||
'English': 'Bullet',
|
||||
'Slovenian': 'Zadetek',
|
||||
},
|
||||
|
||||
// Bullet color
|
||||
'Select Bullet Color': {
|
||||
'English': 'Select Bullet Color',
|
||||
'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': {
|
||||
'English': 'Camera Settings',
|
||||
@@ -187,10 +307,6 @@ class AppTranslations {
|
||||
'English': 'Tournament',
|
||||
'Slovenian': 'Tekmovanje',
|
||||
},
|
||||
'Last used folder': {
|
||||
'English': 'Last used folder',
|
||||
'Slovenian': 'Zadnja uporabljena mapa',
|
||||
},
|
||||
'New folder': {
|
||||
'English': 'New folder',
|
||||
'Slovenian': 'Nova mapa',
|
||||
@@ -221,10 +337,18 @@ class AppTranslations {
|
||||
'English': 'Tournament Mode 🏆',
|
||||
'Slovenian': 'Tekmovalni način 🏆',
|
||||
},
|
||||
'Tournament Mode': {
|
||||
'English': 'Tournament Mode',
|
||||
'Slovenian': 'Tekmovalni način',
|
||||
},
|
||||
'Mark as Tournament 🏆': {
|
||||
'English': 'Mark as Tournament 🏆',
|
||||
'Slovenian': 'Označi kot tekmovanje 🏆',
|
||||
},
|
||||
'Mark as Tournament': {
|
||||
'English': 'Mark as Tournament',
|
||||
'Slovenian': 'Označi kot tekmovanje',
|
||||
},
|
||||
'Apply': {
|
||||
'English': 'Apply',
|
||||
'Slovenian': 'Uporabi',
|
||||
@@ -331,6 +455,10 @@ class AppTranslations {
|
||||
'English': 'Total score',
|
||||
'Slovenian': 'Rezultat:',
|
||||
},
|
||||
'Total score:': {
|
||||
'English': 'Total score:',
|
||||
'Slovenian': 'Skupni seštevek:',
|
||||
},
|
||||
|
||||
// Stream Screen (Camera View)
|
||||
'Choose a Filter': {
|
||||
@@ -584,45 +712,189 @@ class AppTranslations {
|
||||
'English': 'copied to clipboard',
|
||||
'Slovenian': 'kopirano v odložišče',
|
||||
},
|
||||
|
||||
// App Update translations
|
||||
'Update Available': {
|
||||
'English': 'Update Available',
|
||||
'Slovenian': 'Posodobitev je na voljo',
|
||||
'Select Cameras to Spectate': {
|
||||
'English': 'Select Cameras to Spectate',
|
||||
'Slovenian': 'Izberi kamere za ogled',
|
||||
},
|
||||
'A new version of the app is available': {
|
||||
'English': 'A new version of the app is available',
|
||||
'Slovenian': 'Nova različica aplikacije je na voljo',
|
||||
'Choose 1-6 cameras': {
|
||||
'English': 'Choose 1-6 cameras',
|
||||
'Slovenian': 'Izberi 1-6 kamer',
|
||||
},
|
||||
'Update': {
|
||||
'English': 'Update',
|
||||
'Slovenian': 'Posodobi',
|
||||
'No cameras configured': {
|
||||
'English': 'No cameras configured',
|
||||
'Slovenian': 'Ni nastavljenih kamer',
|
||||
},
|
||||
'Later': {
|
||||
'English': 'Later',
|
||||
'Slovenian': 'Kasneje',
|
||||
'View Cameras': {
|
||||
'English': 'View Cameras',
|
||||
'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
|
||||
'Check for Updates': {
|
||||
'English': 'Check for Updates',
|
||||
'Slovenian': 'Preveri za posodobitve',
|
||||
// Analytics
|
||||
'Analytics': {
|
||||
'English': 'Analytics',
|
||||
'Slovenian': 'Analitika',
|
||||
},
|
||||
'No updates available': {
|
||||
'English': 'No updates available',
|
||||
'Slovenian': 'Ni dostopnih posodobitev',
|
||||
'Compare Sessions': {
|
||||
'English': 'Compare Sessions',
|
||||
'Slovenian': 'Primerjaj treninge',
|
||||
},
|
||||
'You are using the latest version': {
|
||||
'English': 'You are using the latest version',
|
||||
'Slovenian': 'Uporabljate najnovejšo različico',
|
||||
'Select 2-4 sessions': {
|
||||
'English': 'Select 2-4 sessions to compare',
|
||||
'Slovenian': 'Izberi 2-4 treninge za primerjavo',
|
||||
},
|
||||
'Updates': {
|
||||
'English': 'Updates',
|
||||
'Slovenian': 'Posodobitve',
|
||||
'Select at least 2 sessions': {
|
||||
'English': 'Select at least 2 sessions',
|
||||
'Slovenian': 'Izberi vsaj 2 treninga',
|
||||
},
|
||||
'OK': {
|
||||
'English': 'OK',
|
||||
'Slovenian': 'V redu',
|
||||
'Max 4 sessions': {
|
||||
'English': 'Maximum 4 sessions can be compared',
|
||||
'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
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -137,14 +137,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -165,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -254,6 +246,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -283,6 +299,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -352,14 +376,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -468,26 +484,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -500,26 +516,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -528,14 +544,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -801,10 +809,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.10"
|
||||
tflite_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -813,6 +821,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -821,14 +837,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -921,18 +929,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: version
|
||||
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "2.2.0"
|
||||
visibility_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1038,5 +1038,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
dart: ">=3.9.0-0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
@@ -2,7 +2,7 @@ name: test1
|
||||
description: A new Flutter project.
|
||||
publish_to: 'none' # This prevents publishing to pub.dev
|
||||
|
||||
version: 1.0.2+1
|
||||
version: 1.1.0+4
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
@@ -35,7 +35,9 @@ dependencies:
|
||||
image_picker: ^1.0.4
|
||||
wakelock_plus: ^1.2.11
|
||||
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:
|
||||
http: ^0.13.6
|
||||
@@ -58,5 +60,7 @@ flutter:
|
||||
- assets/logo.png
|
||||
- assets/logo.svg
|
||||
- assets/target.png
|
||||
- assets/target.svg
|
||||
- assets/google_logo.png
|
||||
- assets/best_float32_nms.tflite
|
||||
- assets/targets/
|
||||
|
||||