Notifications added to the app -> updated version to V1.1.0
This commit is contained in:
@@ -14,6 +14,7 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -22,7 +23,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.example.shooter_app"
|
applicationId = "com.example.shooter_app"
|
||||||
minSdk = 26 // ✅ From earlier fix
|
minSdk = 26
|
||||||
targetSdk = 33
|
targetSdk = 33
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
@@ -31,12 +32,18 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+10
@@ -16,3 +16,13 @@
|
|||||||
|
|
||||||
# Keep your YOLO model loader class if needed
|
# Keep your YOLO model loader class if needed
|
||||||
-keep class your.package.name.YOLODetect { *; }
|
-keep class your.package.name.YOLODetect { *; }
|
||||||
|
|
||||||
|
# Flutter Local Notifications
|
||||||
|
-keep class com.dexterous.** { *; }
|
||||||
|
-dontwarn com.dexterous.**
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
-keep class com.jakewharton.threetenabp.** { *; }
|
||||||
|
-dontwarn com.jakewharton.threetenabp.**
|
||||||
|
-keep class org.threeten.** { *; }
|
||||||
|
-dontwarn org.threeten.**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -41,6 +42,15 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
|
||||||
|
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
|
||||||
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!-- Outer circle -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:pathData="M12,2 A10,10 0 1,1 11.999,2 Z"/>
|
||||||
|
<!-- Middle circle -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:pathData="M12,6 A6,6 0 1,1 11.999,6 Z"/>
|
||||||
|
<!-- Inner circle -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,9 A3,3 0 1,1 11.999,9 Z"/>
|
||||||
|
<!-- Crosshair top -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:pathData="M12,2 L12,0"/>
|
||||||
|
<!-- Crosshair bottom -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:pathData="M12,22 L12,24"/>
|
||||||
|
<!-- Crosshair left -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:pathData="M2,12 L0,12"/>
|
||||||
|
<!-- Crosshair right -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:pathData="M22,12 L24,12"/>
|
||||||
|
</vector>
|
||||||
@@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.7.0" apply false
|
id("com.android.application") version "8.7.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart'; // Import for SystemChrome
|
import 'package:flutter/services.dart'; // Import for SystemChrome
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
|
import 'services/notification_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
try {
|
||||||
|
await NotificationService().initialize();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
// Lock orientation to portrait only
|
// Lock orientation to portrait only
|
||||||
await SystemChrome.setPreferredOrientations([
|
await SystemChrome.setPreferredOrientations([
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:intl/date_symbol_data_local.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../services/google_calendar_service.dart';
|
import '../services/google_calendar_service.dart';
|
||||||
|
import '../services/notification_service.dart';
|
||||||
import '../utils/translations.dart';
|
import '../utils/translations.dart';
|
||||||
import '../utils/target_colors.dart';
|
import '../utils/target_colors.dart';
|
||||||
|
|
||||||
@@ -234,6 +235,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
_events = filteredEvents;
|
_events = filteredEvents;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
NotificationService().scheduleEventNotifications(filteredEvents);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
padding: const EdgeInsets.only(right: 16.0),
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'V1.0.3',
|
'V1.1',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
color: Colors.grey[400],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../utils/translations.dart';
|
import '../utils/translations.dart';
|
||||||
import '../utils/target_colors.dart';
|
import '../utils/target_colors.dart';
|
||||||
|
import '../services/notification_service.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
@@ -43,6 +44,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'Grey',
|
'Grey',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
int _notificationHours = 1;
|
||||||
|
final List<int> _notificationHourOptions = [1, 2, 3, 6, 12, 24, 48];
|
||||||
|
|
||||||
|
bool _notifyDezuranje = true;
|
||||||
|
bool _notifySestanek = true;
|
||||||
|
bool _notifyTekma = true;
|
||||||
|
|
||||||
String _selectedLanguage = 'English';
|
String _selectedLanguage = 'English';
|
||||||
final List<Map<String, String>> _languages = [
|
final List<Map<String, String>> _languages = [
|
||||||
{'code': 'en', 'name': 'English', 'flag': '🇬🇧'},
|
{'code': 'en', 'name': 'English', 'flag': '🇬🇧'},
|
||||||
@@ -96,6 +104,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_loadBulletColor();
|
_loadBulletColor();
|
||||||
_loadLanguage();
|
_loadLanguage();
|
||||||
_loadSelectedTarget();
|
_loadSelectedTarget();
|
||||||
|
_loadNotificationSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -205,6 +214,28 @@ String _getTargetDisplayName(String targetPath) {
|
|||||||
return _t(targetName);
|
return _t(targetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadNotificationSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
setState(() {
|
||||||
|
_notificationHours = prefs.getInt('notificationHours_single') ?? 1;
|
||||||
|
_notifyDezuranje = prefs.getBool('notifyDezuranje') ?? true;
|
||||||
|
_notifySestanek = prefs.getBool('notifySestanek') ?? true;
|
||||||
|
_notifyTekma = prefs.getBool('notifyTekma') ?? true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveNotificationHours(int hours) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setInt('notificationHours_single', hours);
|
||||||
|
await prefs.setStringList('notificationHours', [hours.toString()]);
|
||||||
|
setState(() => _notificationHours = hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveNotifyType(String key, bool value) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadCameraIPs() async {
|
Future<void> _loadCameraIPs() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@@ -388,6 +419,8 @@ String _getTargetDisplayName(String targetPath) {
|
|||||||
_buildAppearanceSelector(),
|
_buildAppearanceSelector(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
_buildNotificationSettings(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
_buildCameraList(),
|
_buildCameraList(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1096,4 +1129,180 @@ void _showLanguagePopup() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildNotifyToggle(String label, bool value, ValueChanged<bool> onChanged) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(color: Colors.black87, fontSize: 14)),
|
||||||
|
Switch(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
activeThumbColor: _getColorFromName(_selectedBulletColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNotificationSettings() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[900],
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(12),
|
||||||
|
topRight: Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_t('Notifications'),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
await NotificationService().scheduleTestNotification();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.notifications_active_outlined, color: Colors.white, size: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Hours before picker
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(12),
|
||||||
|
bottomRight: Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_t('Event types'),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildNotifyToggle(_t('Dežuranje'), _notifyDezuranje, (v) {
|
||||||
|
setState(() => _notifyDezuranje = v);
|
||||||
|
_saveNotifyType('notifyDezuranje', v);
|
||||||
|
}),
|
||||||
|
_buildNotifyToggle(_t('Sestanek'), _notifySestanek, (v) {
|
||||||
|
setState(() => _notifySestanek = v);
|
||||||
|
_saveNotifyType('notifySestanek', v);
|
||||||
|
}),
|
||||||
|
_buildNotifyToggle(_t('Tekma'), _notifyTekma, (v) {
|
||||||
|
setState(() => _notifyTekma = v);
|
||||||
|
_saveNotifyType('notifyTekma', v);
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_t('Notify before event'),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_notificationHours == 1
|
||||||
|
? '1 ${_t('hour')}'
|
||||||
|
: '$_notificationHours ${_t('hours')}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: _getColorFromName(_selectedBulletColor),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Row(
|
||||||
|
children: _notificationHourOptions.asMap().entries.map((entry) {
|
||||||
|
final i = entry.key;
|
||||||
|
final hours = entry.value;
|
||||||
|
final isSelected = hours == _notificationHours;
|
||||||
|
final label = '${hours}h';
|
||||||
|
final isFirst = i == 0;
|
||||||
|
final isLast = i == _notificationHourOptions.length - 1;
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => _saveNotificationHours(hours),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? _getColorFromName(_selectedBulletColor)
|
||||||
|
: Colors.grey[700],
|
||||||
|
border: Border(
|
||||||
|
right: isLast
|
||||||
|
? BorderSide.none
|
||||||
|
: BorderSide(color: Colors.grey[600]!, width: 1),
|
||||||
|
),
|
||||||
|
borderRadius: isFirst
|
||||||
|
? const BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10))
|
||||||
|
: isLast
|
||||||
|
? const BorderRadius.only(topRight: Radius.circular(10), bottomRight: Radius.circular(10))
|
||||||
|
: BorderRadius.zero,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -846,6 +846,56 @@ class AppTranslations {
|
|||||||
'English': 'Points',
|
'English': 'Points',
|
||||||
'Slovenian': 'Točke',
|
'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',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
static String translate(String key, String language) {
|
static String translate(String key, String language) {
|
||||||
|
|||||||
+49
-9
@@ -85,10 +85,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -246,6 +246,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_local_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications
|
||||||
|
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "18.0.1"
|
||||||
|
flutter_local_notifications_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_linux
|
||||||
|
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
|
flutter_local_notifications_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_platform_interface
|
||||||
|
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.0"
|
||||||
flutter_mjpeg:
|
flutter_mjpeg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -275,6 +299,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_timezone:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_timezone
|
||||||
|
sha256: ea53c61c9152f271a5e30624a624184804947b6a733ff2b64186bb2579446892
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -484,18 +516,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -777,10 +809,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.10"
|
||||||
tflite_flutter:
|
tflite_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -789,6 +821,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.0"
|
version: "0.11.0"
|
||||||
|
timezone:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -998,5 +1038,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.9.0-0 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.29.0"
|
||||||
|
|||||||
+4
-1
@@ -2,7 +2,7 @@ name: test1
|
|||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
publish_to: 'none' # This prevents publishing to pub.dev
|
publish_to: 'none' # This prevents publishing to pub.dev
|
||||||
|
|
||||||
version: 1.0.3+3
|
version: 1.1.0+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
@@ -35,6 +35,9 @@ dependencies:
|
|||||||
image_picker: ^1.0.4
|
image_picker: ^1.0.4
|
||||||
wakelock_plus: ^1.2.11
|
wakelock_plus: ^1.2.11
|
||||||
url_launcher: ^6.1.0
|
url_launcher: ^6.1.0
|
||||||
|
flutter_local_notifications: ^18.0.1
|
||||||
|
timezone: ^0.9.4
|
||||||
|
flutter_timezone: ^3.0.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
http: ^0.13.6
|
http: ^0.13.6
|
||||||
|
|||||||
Reference in New Issue
Block a user