contac and calendar update

This commit is contained in:
2025-11-08 12:26:18 +01:00
parent 05044691b6
commit 8359b8c49e
13 changed files with 1037 additions and 532 deletions
-55
View File
@@ -1,55 +0,0 @@
# Changelog
All notable changes to the Shooting Target Detection SDK will be documented in this file.
## [1.0.0] - 2025-10-18
### Added
- 🎯 **YOLO-based Bullet Detection** - AI-powered bullet hole recognition
- 📷 **MJPEG Camera Streaming** - Real-time camera feed support
- 📱 **Phone Camera Integration** - Device camera for target capture
- 🎨 **Image Processing** - Brightness, contrast, and filter controls
- 📊 **Target Scoring System** - Automatic ring detection and scoring
- 🌐 **Multi-language Support** - English and Slovenian
- 📅 **Google Calendar Integration** - Track shooting sessions
- 🍎 **iOS Support** - Configured and ready for iOS build
- 📖 **Help System** - In-app help dialogs with translations
- 💾 **Settings Persistence** - Save brightness/contrast preferences
### Fixed
- 🐛 Color filters no longer apply to error messages
- 🐛 Zoom functionality disabled on error screens
- 🐛 Removed duplicate "NOW" badge in calendar
- 🐛 Proper error handling for camera stream failures
### Technical Details
- **Flutter Version**: 3.29.3
- **Dart SDK**: 3.3.0+
- **Min Android SDK**: 21 (Android 5.0)
- **Target Android SDK**: 35 (Android 15)
- **APK Size**: 96.4 MB
- **AAB Size**: 48.2 MB
### Package Contents
```
SDK_v1.0.0/
├── android/
│ ├── app-release.apk (96.4 MB)
│ └── app-release.aab (48.2 MB)
├── docs/
├── README.md
└── CHANGELOG.md
```
### Dependencies
- `tflite_flutter: ^0.11.0` - TensorFlow Lite for AI model
- `camera: ^0.10.0` - Camera access
- `flutter_mjpeg: ^2.0.0` - MJPEG streaming
- `google_sign_in: ^6.1.5` - Google authentication
- `googleapis: ^13.2.0` - Calendar API
- `image: ^4.5.4` - Image processing
- `shared_preferences: ^2.2.2` - Settings storage
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
-207
View File
@@ -1,207 +0,0 @@
# Shooting Target Detection SDK v1.0.0
Android SDK for bullet hole detection and shooting target analysis.
## 📦 Package Contents
```
SDK_v1.0.0/
├── android/
│ ├── app-release.apk # Release APK (96.4 MB)
│ └── app-release.aab # App Bundle for Google Play (48.2 MB)
├── docs/
│ └── API_DOCUMENTATION.md
└── README.md
```
## 🎯 Features
- **YOLO-based Bullet Detection** - AI-powered bullet hole recognition using TensorFlow Lite
- **MJPEG Camera Streaming** - Real-time camera feed support with HTTP/HTTPS streams
- **Phone Camera Integration** - Use device camera for target capture
- **Image Processing** - Brightness, contrast, and filter adjustments
- **Target Scoring System** - Automatic ring detection and bullet scoring
- **Multi-language Support** - English and Slovenian translations
- **Google Calendar Integration** - Track shooting sessions
- **Customizable UI** - Material Design with dark theme support
## 📱 System Requirements
### Minimum Requirements
- **Android Version**: 5.0 (API 21) or higher
- **RAM**: 2GB minimum, 4GB recommended
- **Storage**: 150MB free space
- **Camera**: Any device camera or network camera support
- **Network**: For MJPEG streaming (optional)
### Recommended
- **Android Version**: 8.0 (API 26) or higher
- **RAM**: 4GB or more
- **Processor**: ARM64 or x86_64
- **Camera**: 12MP or higher for best detection accuracy
## 🚀 Installation
### Option 1: Install APK (Direct Installation)
```bash
adb install app-release.apk
```
### Option 2: Deploy via Google Play (AAB)
Upload `app-release.aab` to Google Play Console for distribution.
## 🔧 Configuration
### Permissions Required
The SDK requires the following Android permissions:
```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
```
### Network Configuration
For MJPEG camera streaming, ensure network security config allows cleartext traffic:
```xml
<application
android:usesCleartextTraffic="true">
```
## 📖 Usage
### 1. Camera Stream Detection
```
1. Open the app
2. Navigate to "Camera Streams" section
3. Add your MJPEG camera URL (e.g., http://192.168.1.100:8080/video)
4. Select the camera and start detection
5. Tap capture button to analyze target
```
### 2. Phone Camera Detection
```
1. Open the app
2. Navigate to "Phone Camera" section
3. Set number of expected bullets (1-5)
4. Align target in the frame
5. Capture baseline image (empty target)
6. Shoot your rounds
7. Capture final image to see detected bullets
```
### 3. Target Analysis
```
1. After capture, view detected bullets overlaid on target
2. Check ring detection and scoring
3. View shooting history in calendar
4. Export results to Google Calendar
```
## 🎨 Customization
### Image Adjustments
- **Brightness**: -100 to +100
- **Contrast**: 50% to 200%
- **Filters**: None, Greyscale, Invert
### Detection Settings
- **Bullet Count**: 1-5 expected bullets
- **Zoom**: 1x to 5x for camera streams
- **Auto-stop**: Automatic capture when target count reached
## 🌐 Language Support
The SDK supports:
- 🇬🇧 English
- 🇸🇮 Slovenian (Slovenščina)
Language can be changed in the settings menu.
## 📊 Technical Specifications
### AI Model
- **Framework**: TensorFlow Lite
- **Model**: YOLOv8 (optimized for mobile)
- **Input Size**: 640x640 pixels
- **Detection Classes**:
- Bullet holes
- Target rings (10-ring system)
- Center point
### Image Processing
- **Supported Formats**: JPEG, PNG
- **Max Resolution**: 4096x4096 pixels
- **Processing Time**: < 2 seconds per image (on recommended hardware)
### Camera Support
- **MJPEG Streams**: HTTP/HTTPS streaming
- **Device Camera**: All standard Android cameras
- **Resolution**: Auto-adjusted based on device capabilities
## 🔐 Security & Privacy
- ✅ All processing done on-device (no cloud upload)
- ✅ Camera permissions requested at runtime
- ✅ Optional Google Calendar integration
- ✅ No telemetry or analytics tracking
- ✅ Images stored locally only
## 📝 Version History
### v1.0.0 (2025-10-18)
- ✨ Initial release
- ✅ YOLO bullet detection
- ✅ MJPEG streaming support
- ✅ Phone camera integration
- ✅ Calendar integration
- ✅ Multi-language support
- ✅ iOS support configured
- 🐛 Fixed filter application on error screens
- 🐛 Fixed zoom on error states
- 🐛 Removed duplicate calendar badges
## 🛠️ Troubleshooting
### Camera Stream Not Connecting
- Check camera URL format: `http://IP:PORT/video`
- Ensure camera is on same network
- Verify `usesCleartextTraffic="true"` in AndroidManifest.xml
### Detection Not Working
- Ensure good lighting conditions
- Clean camera lens
- Target should fill at least 50% of frame
- Check that YOLO model is included in assets
### Performance Issues
- Close other apps to free RAM
- Reduce image resolution in settings
- Disable unnecessary features (filters, calendar sync)
## 💻 Development Info
Built with:
- **Flutter**: 3.29.3
- **Dart**: 3.3.0+
- **Android SDK**: 36.0.0
- **Min SDK**: 21 (Android 5.0)
- **Target SDK**: 35 (Android 15)
## 📧 Support
For issues, questions, or feature requests:
- Create an issue in the project repository
- Contact: [Your contact information]
## 📄 License
Copyright © 2025. All rights reserved.
---
🤖 Built with [Claude Code](https://claude.com/claude-code)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -27,7 +27,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustPan">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
+21 -23
View File
@@ -633,7 +633,7 @@ Widget _buildFolderRow(String folderName) {
_folderTypes[folderName] == true
? '🏆 $folderName'
: folderName,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _bulletColor),
),
],
),
@@ -795,8 +795,10 @@ Widget _buildTargetsList(String folderName) {
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Target info section
// Target info section - Clickable
Expanded(
child: InkWell(
onTap: () => _openFilePreview(file),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Row(
@@ -830,19 +832,6 @@ Widget _buildTargetsList(String folderName) {
),
),
),
// Preview button
InkWell(
onTap: () => _openFilePreview(file),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: _bulletColor.withValues(alpha: 0.15),
border: Border(
left: BorderSide(color: Colors.grey[300]!, width: 1),
),
),
child: Icon(Icons.visibility, color: _bulletColor, size: 18),
),
),
// Delete button
InkWell(
@@ -881,17 +870,25 @@ Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// 🔽 Filter dropdown at the top
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
// 🔽 Filter dropdown at the top (styled like tab bar)
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.grey[900],
border: Border(
bottom: BorderSide(color: Colors.grey[800]!, width: 1),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: GestureDetector(
onTap: () => _showFolderFilterPopup(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[300], // Same as folder background
borderRadius: BorderRadius.circular(12),
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -902,14 +899,15 @@ Widget build(BuildContext context) {
: _folderFilter == 'tournament'
? _t('Showing: 🏆 Tournament folders')
: _t('Showing: 🎯 Training folders'),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black),
),
const Icon(Icons.filter_list),
Icon(Icons.filter_list, color: Colors.black87, size: 18),
],
),
),
),
),
),
// 📁 Folder list or empty state
Expanded(
child: _groupedFiles.isEmpty
+676 -37
View File
@@ -1,10 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
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 '../utils/translations.dart';
class Contact {
final String firstName;
final String lastName;
final String phone;
final String email;
Contact({
required this.firstName,
required this.lastName,
required this.phone,
required this.email,
});
String get fullName => '$firstName $lastName';
}
enum EventType {
regular, // Dežurstvo and other regular events
competition, // Liga and Tekmovanje (League and Competition events)
@@ -25,6 +43,27 @@ class _CalendarScreenState extends State<CalendarScreen> {
String? _errorMessage;
String _bulletColor = 'Blue';
String _selectedLanguage = 'English';
bool _showContacts = false;
bool _showDebugPreview = true;
// Members contact list
final List<Contact> _contacts = [
Contact(firstName: 'Domen', lastName: 'Pleterski', phone: '051227764', email: 'info@bl3k.com'),
Contact(firstName: 'Ivan', lastName: 'Tandler', phone: '040471258', email: 'ivan.tandler@gmail.com'),
Contact(firstName: 'Jože', lastName: 'Verhnjak', phone: '041443463', email: 'joze.verhnjak@gmail.com'),
Contact(firstName: 'Branko', lastName: 'Pokeržnik', phone: '041291001', email: 'branko.pokerznik@gmail.com'),
Contact(firstName: 'Franc', lastName: 'Žigart', phone: '041439597', email: 'franc.zigart@gmail.com'),
Contact(firstName: 'Janez', lastName: 'Božič', phone: '041551704', email: 'janbozic@gmail.com'),
Contact(firstName: 'Mitja', lastName: 'Čeh', phone: '070550383', email: 'mekabara@gmail.com'),
Contact(firstName: 'Rado', lastName: 'Keter', phone: '041324529', email: 'rado.keter@gmail.com'),
Contact(firstName: 'Karli', lastName: 'Proje', phone: '040424951', email: 'karl.project@gmail.com'),
Contact(firstName: 'Jan', lastName: 'Pleterski', phone: '041842063', email: 'jan.pleterski1997@gmail.com'),
Contact(firstName: 'Silvo', lastName: 'Poročnik', phone: '040860618', email: 'skobar.silvo@gmail.com'),
Contact(firstName: 'Dušan', lastName: 'Onuk', phone: '070448303', email: 'dusan.onuk@gmail.com'),
Contact(firstName: 'Matjaž', lastName: 'Pleterski', phone: '041603859', email: 'matjaz.pleterski@gmail.com'),
Contact(firstName: 'Marko', lastName: 'Blimen', phone: '031610857', email: 'blimenmarko@gmail.com'),
Contact(firstName: 'Doris', lastName: 'Fesel', phone: '070373987', email: 'doris.fesel@gmail.com'),
];
@override
void initState() {
@@ -46,6 +85,12 @@ class _CalendarScreenState extends State<CalendarScreen> {
});
}
void _toggleDebugPreview() {
setState(() {
_showDebugPreview = !_showDebugPreview;
});
}
@override
void dispose() {
_calendarService.dispose();
@@ -113,8 +158,8 @@ class _CalendarScreenState extends State<CalendarScreen> {
return true;
}
EventType _getEventType(CalendarEvent event) {
if (event.summary == null) return EventType.regular;
EventType? _getEventType(CalendarEvent event) {
if (event.summary == null) return null;
final summary = event.summary!.toLowerCase();
@@ -122,9 +167,12 @@ class _CalendarScreenState extends State<CalendarScreen> {
return EventType.competition;
} else if (summary.contains('sestanek')) {
return EventType.sestanek;
} else if (summary.contains('dežurstvo')) {
return EventType.regular;
}
return EventType.regular;
// No matching keywords - skip this event
return null;
}
Future<void> _loadEvents() async {
@@ -143,10 +191,13 @@ class _CalendarScreenState extends State<CalendarScreen> {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
// Filter: valid events AND events from today onwards
// Filter: valid events AND matching keywords AND events from today onwards
final filteredEvents = events.where((event) {
if (!_isValidEvent(event)) return false;
// Check if event has a matching keyword type
if (_getEventType(event) == null) return false;
// Only include events that are today or in the future
if (event.startTime != null) {
final eventDate = DateTime(
@@ -183,10 +234,448 @@ class _CalendarScreenState extends State<CalendarScreen> {
}
}
Future<void> _makePhoneCall(String phoneNumber) async {
final Uri launchUri = Uri(
scheme: 'tel',
path: phoneNumber,
);
try {
await launchUrl(launchUri);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${_t('Could not launch phone call: ')}$e')),
);
}
}
}
String _capitalize(String text) {
return text.split(' ').map((word) {
if (word.isEmpty) return word;
return word[0].toUpperCase() + word.substring(1).toLowerCase();
}).join(' ');
}
void _copyToClipboard(String text, String label) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$label ${_t('copied to clipboard')}'),
duration: const Duration(seconds: 2),
),
);
}
void _showContactPopup(BuildContext context, Contact contact) {
final accentColor = _getAccentColor();
showModalBottomSheet(
context: context,
backgroundColor: Colors.grey[900],
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return RepaintBoundary(
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with contact name
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
contact.firstName[0].toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_capitalize(contact.fullName),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
// Phone number row
Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
decoration: BoxDecoration(
color: Colors.grey[850],
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(Icons.phone, color: accentColor, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_t('Phone'),
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
),
),
Text(
contact.phone,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
Container(
decoration: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_copyToClipboard(contact.phone, 'Phone number');
Navigator.pop(context);
},
borderRadius: BorderRadius.circular(8),
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(
Icons.content_copy,
color: Colors.white,
size: 18,
),
),
),
),
),
],
),
),
const SizedBox(height: 12),
// Email row
Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
decoration: BoxDecoration(
color: Colors.grey[850],
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(Icons.email, color: accentColor, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_t('Email'),
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
),
),
Text(
contact.email.toLowerCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Container(
decoration: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_copyToClipboard(contact.email, 'Email address');
Navigator.pop(context);
},
borderRadius: BorderRadius.circular(8),
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(
Icons.content_copy,
color: Colors.white,
size: 18,
),
),
),
),
),
],
),
),
],
),
),
);
},
);
}
Widget _buildContactsList() {
final accentColor = _getAccentColor();
return ListView.builder(
itemCount: _contacts.length,
padding: const EdgeInsets.all(8),
itemBuilder: (context, index) {
final contact = _contacts[index];
return GestureDetector(
onTap: () => _makePhoneCall(contact.phone),
onLongPress: () => _showContactPopup(context, contact),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.08),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Colored header bar (like event cards)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
child: Row(
children: [
// Small colored indicator
Container(
width: 3,
height: 12,
decoration: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 7),
// Contact name
Expanded(
child: Text(
_capitalize(contact.fullName),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: accentColor,
),
),
),
],
),
),
// Main content with phone and email info
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.phone, size: 14, color: accentColor),
const SizedBox(width: 6),
Text(
contact.phone,
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.email, size: 14, color: accentColor),
const SizedBox(width: 6),
Expanded(
child: Text(
contact.email.toLowerCase(),
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
],
),
// Phone button on the right (wider now)
Positioned(
right: 0,
top: 0,
bottom: 0,
child: Container(
width: 80,
decoration: BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _makePhoneCall(contact.phone),
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
child: Center(
child: Icon(
Icons.call,
color: Colors.white,
size: 22,
),
),
),
),
),
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final accentColor = _getAccentColor();
// Debug preview - disabled by default, set _showDebugPreview = true to enable
if (_showDebugPreview) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.grey[900],
centerTitle: true,
title: const Text(
'Debug Preview',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: _toggleDebugPreview,
),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 8),
// Regular event
Text(
'Regular Event (Dežurstvo)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
_buildSampleEventCard(accentColor, EventType.regular),
const SizedBox(height: 24),
// Competition event
Text(
'Competition Event (Tekmovanje)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
_buildSampleEventCard(accentColor, EventType.competition),
const SizedBox(height: 24),
// Meeting event
Text(
'Meeting Event (Sestanek)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
_buildSampleEventCard(accentColor, EventType.sestanek),
const SizedBox(height: 24),
],
),
);
}
if (_isLoading && _events.isEmpty) {
return Scaffold(
body: Center(
@@ -229,7 +718,134 @@ class _CalendarScreenState extends State<CalendarScreen> {
return Scaffold(
backgroundColor: Colors.white,
body: RefreshIndicator(
body: Column(
children: [
// Tab buttons
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.grey[900],
border: Border(
bottom: BorderSide(color: Colors.grey[800]!, width: 1),
),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_showContacts = false;
});
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
decoration: BoxDecoration(
color: !_showContacts ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.calendar_month,
color: !_showContacts ? Colors.black : Colors.grey[400],
size: 18,
),
const SizedBox(width: 6),
Text(
_t('Calendar'),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: !_showContacts ? Colors.black : Colors.grey[400],
),
),
],
),
),
),
),
const SizedBox(width: 6),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_showContacts = true;
});
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
decoration: BoxDecoration(
color: _showContacts ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.people,
color: _showContacts ? Colors.black : Colors.grey[400],
size: 18,
),
const SizedBox(width: 6),
Text(
_t('Contacts'),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _showContacts ? Colors.black : Colors.grey[400],
),
),
],
),
),
),
),
if (_showDebugPreview) ...[
const SizedBox(width: 6),
GestureDetector(
onTap: _toggleDebugPreview,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.amber, width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bug_report,
color: Colors.amber,
size: 16,
),
const SizedBox(width: 4),
const Text(
'Debug',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.amber,
),
),
],
),
),
),
],
],
),
),
// Body content
Expanded(
child: _showContacts
? _buildContactsList()
: RefreshIndicator(
onRefresh: _loadEvents,
color: accentColor,
child: _events.isEmpty
@@ -284,6 +900,9 @@ class _CalendarScreenState extends State<CalendarScreen> {
},
),
),
),
],
),
);
}
@@ -297,30 +916,45 @@ class _CalendarScreenState extends State<CalendarScreen> {
switch (eventType) {
case EventType.competition:
cardAccentColor = Colors.green;
cardAccentColor = Colors.orange;
eventIcon = Icons.emoji_events;
title = _selectedLanguage == 'Slovenian' ? 'Liga / Tekmovanje' : 'League / Competition';
title = _t('League / Competition');
time = '09:00 - 18:00';
break;
case EventType.sestanek:
cardAccentColor = Colors.purple;
cardAccentColor = Colors.red;
eventIcon = Icons.groups;
title = _selectedLanguage == 'Slovenian' ? 'Sestanek ekipe' : 'Team Meeting';
title = _t('Team Meeting');
time = '18:00 - 19:00';
break;
case EventType.regular:
cardAccentColor = Colors.blue;
eventIcon = Icons.event;
title = _selectedLanguage == 'Slovenian' ? 'Dežurstvo' : 'Duty Shift';
title = _t('Duty Shift');
time = '14:00 - 16:00';
break;
}
return Container(
return RepaintBoundary(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
// Card glow effect for today's events
BoxShadow(
color: cardAccentColor.withValues(alpha: 0.3),
blurRadius: 16,
offset: const Offset(0, 4),
),
// Subtle inner shadow
BoxShadow(
color: Colors.grey.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -363,13 +997,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
decoration: BoxDecoration(
color: cardAccentColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: cardAccentColor.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -378,10 +1005,10 @@ class _CalendarScreenState extends State<CalendarScreen> {
const SizedBox(width: 4),
Text(
eventType == EventType.competition
? (_selectedLanguage == 'Slovenian' ? 'TEKMOVANJE' : 'COMPETITION')
? _t('TEKMOVANJE')
: eventType == EventType.sestanek
? (_selectedLanguage == 'Slovenian' ? 'SESTANEK' : 'MEETING')
: (_selectedLanguage == 'Slovenian' ? 'DEŽURSTVO' : 'DUTY'),
? _t('SESTANEK')
: _t('DEŽURSTVO'),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
@@ -429,9 +1056,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
// Location
_buildDetailRowInline(
icon: Icons.location_on,
value: _selectedLanguage == 'Slovenian'
? 'Strelišče Ljubljana'
: 'Shooting Range',
value: _t('Strelišče Ljubljana'),
accentColor: cardAccentColor,
),
],
@@ -439,6 +1064,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
),
],
),
),
);
}
@@ -454,12 +1080,12 @@ class _CalendarScreenState extends State<CalendarScreen> {
switch (eventType) {
case EventType.competition:
cardAccentColor = Colors.green;
cardAccentColor = Colors.orange;
backgroundColor = Colors.grey[300]!;
eventIcon = Icons.emoji_events; // Trophy icon for competitions
break;
case EventType.sestanek:
cardAccentColor = Colors.purple;
cardAccentColor = Colors.red;
backgroundColor = Colors.grey[300]!;
eventIcon = Icons.groups; // People icon for meetings
break;
@@ -468,6 +1094,9 @@ class _CalendarScreenState extends State<CalendarScreen> {
backgroundColor = Colors.grey[300]!;
eventIcon = Icons.event;
break;
case null:
// This case should never happen due to filtering, but required by compiler
return const SizedBox();
}
// Format the date for display
@@ -495,11 +1124,27 @@ class _CalendarScreenState extends State<CalendarScreen> {
}
}
return Container(
return RepaintBoundary(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
// Card glow effect (stronger for today's events)
if (isToday)
BoxShadow(
color: cardAccentColor.withValues(alpha: 0.3),
blurRadius: 16,
offset: const Offset(0, 4),
),
// Subtle inner shadow
BoxShadow(
color: Colors.grey.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -544,13 +1189,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
decoration: BoxDecoration(
color: cardAccentColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: cardAccentColor.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -559,10 +1197,10 @@ class _CalendarScreenState extends State<CalendarScreen> {
const SizedBox(width: 4),
Text(
eventType == EventType.competition
? (_selectedLanguage == 'Slovenian' ? 'TEKMOVANJE' : 'COMPETITION')
? _t('TEKMOVANJE')
: eventType == EventType.sestanek
? (_selectedLanguage == 'Slovenian' ? 'SESTANEK' : 'MEETING')
: (_selectedLanguage == 'Slovenian' ? 'DEŽURSTVO' : 'DUTY'),
? _t('SESTANEK')
: _t('DEŽURSTVO'),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
@@ -630,6 +1268,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
),
],
),
),
);
}
+3 -1
View File
@@ -178,7 +178,8 @@ Future<void> _saveLanguage(String language) async {
showDialog(
context: context,
builder: (context) {
return Dialog(
return RepaintBoundary(
child: Dialog(
backgroundColor: Colors.grey[900],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
@@ -295,6 +296,7 @@ Future<void> _saveLanguage(String language) async {
},
),
),
),
);
},
);
+58
View File
@@ -526,6 +526,64 @@ class AppTranslations {
'English': 'Done',
'Slovenian': 'Končano',
},
// Calendar Screen - New translations
'Contacts': {
'English': 'Contacts',
'Slovenian': 'Stiki',
},
'Could not launch phone call: ': {
'English': 'Could not launch phone call: ',
'Slovenian': 'Ne morem zagnati klica: ',
},
'League / Competition': {
'English': 'League / Competition',
'Slovenian': 'Liga / Tekmovanje',
},
'Team Meeting': {
'English': 'Team Meeting',
'Slovenian': 'Sestanek ekipe',
},
'Duty Shift': {
'English': 'Duty Shift',
'Slovenian': 'Dežurstvo',
},
'TEKMOVANJE': {
'English': 'COMPETITION',
'Slovenian': 'TEKMOVANJE',
},
'COMPETITION': {
'English': 'COMPETITION',
'Slovenian': 'TEKMOVANJE',
},
'SESTANEK': {
'English': 'MEETING',
'Slovenian': 'SESTANEK',
},
'MEETING': {
'English': 'MEETING',
'Slovenian': 'SESTANEK',
},
'Strelišče Ljubljana': {
'English': 'Shooting Range',
'Slovenian': 'Strelišče Ljubljana',
},
'Shooting Range': {
'English': 'Shooting Range',
'Slovenian': 'Strelišče Ljubljana',
},
'Phone': {
'English': 'Phone',
'Slovenian': 'Telefon',
},
'Email': {
'English': 'Email',
'Slovenian': 'Elektronska pošta',
},
'copied to clipboard': {
'English': 'copied to clipboard',
'Slovenian': 'kopirano v odložišče',
},
};
static String translate(String key, String language) {
@@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
+1
View File
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+65 -1
View File
@@ -797,6 +797,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
url: "https://pub.dev"
source: hosted
version: "6.3.20"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
url: "https://pub.dev"
source: hosted
version: "6.3.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
url: "https://pub.dev"
source: hosted
version: "3.2.3"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
vector_graphics:
dependency: transitive
description:
@@ -935,4 +999,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"
flutter: ">=3.29.0"
+1
View File
@@ -34,6 +34,7 @@ dependencies:
path_provider: ^2.0.15
image_picker: ^1.0.4
wakelock_plus: ^1.2.11
url_launcher: ^6.1.0
dependency_overrides:
http: ^0.13.6