From 93ec1e852fcaab477083e8a6cd96b0b320a8755e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Tue, 31 May 2022 17:55:28 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20pracovat=20na=20ozn=C3=A1men=C3=AD?= =?UTF-8?q?=20p=C5=99ed=20ob=C4=9Bdem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/devcontainer.json | 5 ++ CHANGELOG.md | 3 + lib/lang/lang.dart | 4 ++ lib/lang/lang_cz.dart | 7 ++ lib/lang/lang_en.dart | 6 ++ lib/main.dart | 118 ++++++++++++++++++++++++++++---- lib/okna/nastaveni.dart | 24 +++++++ pubspec.lock | 84 ++++++++++------------- pubspec.yaml | 3 +- 9 files changed, 191 insertions(+), 63 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..abe86c1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "name": "Flutter", + "image": "matspfeiffer/flutter:beta", + "extensions": ["dart-code.dart-code", "dart-code.flutter"] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d31458..7e7e417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 1.3.0 +- Odstranit connectivity_plus +- Přidat možnost oznámení s info o obědu v daný čas # 1.2.0 - Přidat možnost zobrazení oznámení v případě neobjednaného jídla na příští týden - Přidat oznámení o rozbitých uložených údajích diff --git a/lib/lang/lang.dart b/lib/lang/lang.dart index f73158d..6575dba 100644 --- a/lib/lang/lang.dart +++ b/lib/lang/lang.dart @@ -145,6 +145,10 @@ abstract class Languages { String get checkOrdered; + String get notifyLunch; + + String get notifyAt; + // Offline String get offline; diff --git a/lib/lang/lang_cz.dart b/lib/lang/lang_cz.dart index fd77801..baffcba 100644 --- a/lib/lang/lang_cz.dart +++ b/lib/lang/lang_cz.dart @@ -207,4 +207,11 @@ class LanguageCz extends Languages { @override String get corrupted => "Nastal problém s dešifrováním uložených údajů, prosím zkuste vyčistit veškerá data této aplikace."; + + @override + String get notifyAt => "Odeslat v"; + + @override + String get notifyLunch => + "V určený čas odeslat notifikaci s informacemi o obědě"; } diff --git a/lib/lang/lang_en.dart b/lib/lang/lang_en.dart index 3b838db..9bd4183 100644 --- a/lib/lang/lang_en.dart +++ b/lib/lang/lang_en.dart @@ -206,4 +206,10 @@ class LanguageEn extends Languages { @override String get corrupted => "The saved credentials seem to be corrupted, please try clearing the application's data."; + + @override + String get notifyAt => "Send notification at"; + + @override + String get notifyLunch => "Send a notification with meal info"; } diff --git a/lib/main.dart b/lib/main.dart index 2adb7f6..6b1c41c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,20 @@ import 'dart:convert'; import 'dart:io'; +import 'package:background_fetch/background_fetch.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:opencanteen/lang/lang_cz.dart'; import 'package:opencanteen/loginmanager.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:canteenlib/canteenlib.dart'; import 'package:opencanteen/okna/offline_jidelnicek.dart'; import 'package:opencanteen/okna/welcome.dart'; import 'package:opencanteen/util.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'lang/lang.dart'; import 'lang/lang_en.dart'; @@ -35,14 +37,75 @@ Copyright (C) 2022 Matyáš Caras a přispěvatelé along with this program. If not, see . */ -void main() { +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + +// Pouze pro Android +void backgroundFetchHeadlessTask(HeadlessTask task) async { + String taskId = task.taskId; + bool isTimeout = task.timeout; + if (isTimeout) { + // Timeout + debugPrint("[BackgroundFetch] Headless task má time-out: $taskId"); + BackgroundFetch.finish(taskId); + return; + } + var prefs = await SharedPreferences.getInstance(); + debugPrint('[BackgroundFetch] Přišel headless event.'); + + if (prefs.getBool("oznamit") ?? false) { + // Oznámení před obědem + if (prefs.getBool("offline") ?? false) { + // TODO možnost brát z offline dat + } else { + // bere online + var d = await LoginManager.getDetails(); // získat údaje + if (d != null) { + // TODO + } + } + } + BackgroundFetch.finish(taskId); +} + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('notif_icon'); + + /// Note: permissions aren't requested here just to demonstrate that can be + /// done later + final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + onDidReceiveLocalNotification: ( + int id, + String? title, + String? body, + String? payload, + ) async { + debugPrint(body); + }); + + final InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); + await flutterLocalNotificationsPlugin.initialize(initializationSettings, + onSelectNotification: (String? payload) async { + if (payload != null) { + debugPrint('notification payload: $payload'); + } + }); runApp(const MyApp()); + BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( @@ -78,22 +141,49 @@ class _LoginPageState extends State { TextEditingController canteenControl = TextEditingController(); bool rememberMe = false; + void nastavitPozadi() async { + // Configure BackgroundFetch. + int status = await BackgroundFetch.configure( + BackgroundFetchConfig( + minimumFetchInterval: 15, + stopOnTerminate: false, + enableHeadless: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + requiredNetworkType: NetworkType.ANY), (String taskId) async { + // Callback + debugPrint("[BackgroundFetch] Event získán $taskId"); + var d = await LoginManager.getDetails(); + if (d != null) { + // TODO + } + BackgroundFetch.finish(taskId); + }, (String taskId) async { + debugPrint("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); + BackgroundFetch.finish(taskId); + }); + debugPrint('[BackgroundFetch] úspěšně nakonfigurováno: $status'); + } + @override void initState() { super.initState(); LoginManager.getDetails().then((r) async { - var connectivityResult = await (Connectivity().checkConnectivity()); - if (connectivityResult == ConnectivityResult.none) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(Languages.of(context)!.errorContacting), - ), - ); - goOffline(); + if (Platform.isIOS) { + // žádat o oprávnění na iOS + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); } - if (r != null) { + // Automaticky přihlásit showDialog( context: context, barrierDismissible: false, @@ -123,6 +213,7 @@ class _LoginPageState extends State { } const storage = FlutterSecureStorage(); var odsouhlasil = await storage.read(key: "oc_souhlas"); + nastavitPozadi(); if (odsouhlasil == null || odsouhlasil != "ano") { Navigator.pushReplacement( context, @@ -260,6 +351,7 @@ class _LoginPageState extends State { LoginManager.setDetails(userControl.text, passControl.text, canteenControl.text); } + nastavitPozadi(); // souhlas const storage = FlutterSecureStorage(); var odsouhlasil = diff --git a/lib/okna/nastaveni.dart b/lib/okna/nastaveni.dart index 8b43e2d..d65ac6a 100644 --- a/lib/okna/nastaveni.dart +++ b/lib/okna/nastaveni.dart @@ -17,6 +17,8 @@ class _NastaveniState extends State { bool _ukladatOffline = false; bool _preskakovatVikend = false; bool _kontrolovatTyden = false; + bool _oznameniObed = false; + String? _oznameniCas; void najitNastaveni() async { var preferences = await SharedPreferences.getInstance(); @@ -24,6 +26,8 @@ class _NastaveniState extends State { _ukladatOffline = preferences.getBool("offline") ?? false; _preskakovatVikend = preferences.getBool("skip") ?? false; _kontrolovatTyden = preferences.getBool("tyden") ?? false; + _oznameniObed = preferences.getBool("oznamit") ?? false; + _oznameniCas = preferences.getString("oznameni_cas"); }); } @@ -91,6 +95,26 @@ class _NastaveniState extends State { }); }) ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(Languages.of(context)!.skipWeekend), + Switch( + value: _oznameniObed, + onChanged: (value) { + setState(() { + _oznameniObed = value; + zmenitNastaveni("oznamit", value); + }); + }) + ], + ), + Text(Languages.of(context)!.notifyAt), + TimePickerDialog( + initialTime: (_oznameniCas == null) + ? TimeOfDay.now() + : TimeOfDay.fromDateTime(DateTime.parse(_oznameniCas!)), ) ], ), diff --git a/pubspec.lock b/pubspec.lock index da74db2..6d92ce2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + background_fetch: + dependency: "direct main" + description: + name: background_fetch + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" canteenlib: dependency: "direct main" description: @@ -57,48 +64,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" - connectivity_plus: - dependency: "direct main" - description: - name: connectivity_plus - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.1" - connectivity_plus_linux: - dependency: transitive - description: - name: connectivity_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - connectivity_plus_macos: - dependency: transitive - description: - name: connectivity_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - connectivity_plus_platform_interface: - dependency: transitive - description: - name: connectivity_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - connectivity_plus_web: - dependency: transitive - description: - name: connectivity_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - connectivity_plus_windows: - dependency: transitive - description: - name: connectivity_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" crypto: dependency: transitive description: @@ -153,6 +118,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "9.5.3+1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -268,13 +254,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" - nm: - dependency: transitive - description: - name: nm - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" path: dependency: transitive description: @@ -441,6 +420,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fde98a3..a5fbbf7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,12 +17,13 @@ dependencies: flutter_localizations: sdk: flutter canteenlib: ^0.1.0-alpha.16 - connectivity_plus: ^2.2.1 flutter_secure_storage: ^5.0.2 url_launcher: ^6.0.20 path_provider: ^2.0.9 shared_preferences: ^2.0.13 introduction_screen: ^3.0.1 + flutter_local_notifications: ^9.5.3+1 + background_fetch: ^1.1.0 dev_dependencies: flutter_lints: ^1.0.0 From e7c41fd124407ab0b16830930aa60e59bab405e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Wed, 8 Jun 2022 09:29:53 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20pr=C3=A1ce=20na=20odesl=C3=A1n?= =?UTF-8?q?=C3=AD=20ozn=C3=A1men=C3=AD=20p=C5=99ed=20ob=C4=9Bdem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/lang/lang.dart | 3 +++ lib/lang/lang_cz.dart | 3 +++ lib/lang/lang_en.dart | 3 +++ lib/main.dart | 59 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/lib/lang/lang.dart b/lib/lang/lang.dart index 6575dba..3dce765 100644 --- a/lib/lang/lang.dart +++ b/lib/lang/lang.dart @@ -153,4 +153,7 @@ abstract class Languages { String get offline; String get mustLogout; + + // Oznámit před obědem + String get lunchNotif; } diff --git a/lib/lang/lang_cz.dart b/lib/lang/lang_cz.dart index baffcba..1fc83a9 100644 --- a/lib/lang/lang_cz.dart +++ b/lib/lang/lang_cz.dart @@ -214,4 +214,7 @@ class LanguageCz extends Languages { @override String get notifyLunch => "V určený čas odeslat notifikaci s informacemi o obědě"; + + @override + String get lunchNotif => "Dnes máte objednáno"; } diff --git a/lib/lang/lang_en.dart b/lib/lang/lang_en.dart index 9bd4183..6c0b887 100644 --- a/lib/lang/lang_en.dart +++ b/lib/lang/lang_en.dart @@ -212,4 +212,7 @@ class LanguageEn extends Languages { @override String get notifyLunch => "Send a notification with meal info"; + + @override + String get lunchNotif => "Today's ordered meal"; } diff --git a/lib/main.dart b/lib/main.dart index 6b1c41c..6ac8052 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:opencanteen/okna/welcome.dart'; import 'package:opencanteen/util.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:intl/intl.dart'; import 'lang/lang.dart'; import 'lang/lang_en.dart'; @@ -40,6 +41,54 @@ Copyright (C) 2022 Matyáš Caras a přispěvatelé final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); +void oznamitPredem(SharedPreferences prefs, {Languages? l}) async { + String title; + if (l == null) { + String locale = Intl.getCurrentLocale(); + switch (locale) { + case "cs_CZ": + title = LanguageCz().lunchNotif; + break; + default: + title = LanguageEn().lunchNotif; + } + } else { + title = l.lunchNotif; + } + if (prefs.getBool("offline") ?? false) { + // TODO možnost brát z offline dat + } else { + // bere online + var d = await LoginManager.getDetails(); // získat údaje + if (d != null) { + var c = Canteen(d["url"]!); + if (await c.login(d["user"]!, d["pass"]!)) { + var jidla = await c.jidelnicekDen(); + try { + var jidlo = jidla.jidla.singleWhere((element) => element.objednano); + var ted = TimeOfDay.now(); + var kdy = prefs.getString("oznameni_cas"); + var cas = TimeOfDay.fromDateTime(DateTime.parse(kdy!)); + if (ted.hour == cas.hour && ted.minute == cas.minute) { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails('opencanteen', 'predobjedem', + channelDescription: 'Oznámení o dnešním jídle', + importance: Importance.max, + priority: Priority.high, + ticker: 'today meal'); + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 0, title, "${jidlo.nazev} - ${jidlo.varianta}", platformChannelSpecifics); + } + } on StateError catch (_) { + // nenalezeno + } + } + } + } +} + // Pouze pro Android void backgroundFetchHeadlessTask(HeadlessTask task) async { String taskId = task.taskId; @@ -55,15 +104,7 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { if (prefs.getBool("oznamit") ?? false) { // Oznámení před obědem - if (prefs.getBool("offline") ?? false) { - // TODO možnost brát z offline dat - } else { - // bere online - var d = await LoginManager.getDetails(); // získat údaje - if (d != null) { - // TODO - } - } + oznamitPredem(prefs); } BackgroundFetch.finish(taskId); } From 93a453463d58c826435d4a7deaff8168f0876476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Wed, 8 Jun 2022 17:12:44 +0200 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20dokon=C4=8Dit=20ozn=C3=A1men=C3=AD?= =?UTF-8?q?=20p=C5=99ed=20ob=C4=9Bdem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 4 +- .../app/src/main/res/drawable/notif_icon.xml | 10 ++ lib/main.dart | 148 +++++++----------- lib/okna/burza.dart | 7 +- lib/okna/jidelnicek.dart | 14 +- lib/okna/nastaveni.dart | 95 +++++++++-- lib/okna/offline_jidelnicek.dart | 4 - lib/okna/welcome.dart | 8 +- lib/util.dart | 15 +- pubspec.lock | 16 +- pubspec.yaml | 3 +- 11 files changed, 195 insertions(+), 129 deletions(-) create mode 100644 android/app/src/main/res/drawable/notif_icon.xml diff --git a/android/app/build.gradle b/android/app/build.gradle index fdd0f6e..6faf358 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,12 +47,12 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "cz.hernikplays.opencanteen" minSdkVersion 18 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } signingConfigs { @@ -76,4 +76,6 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:multidex:1.0.3' + implementation 'androidx.appcompat:appcompat:1.3.1' } diff --git a/android/app/src/main/res/drawable/notif_icon.xml b/android/app/src/main/res/drawable/notif_icon.xml new file mode 100644 index 0000000..ad2d7ef --- /dev/null +++ b/android/app/src/main/res/drawable/notif_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/lib/main.dart b/lib/main.dart index 6ac8052..eb561fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'dart:io'; -import 'package:background_fetch/background_fetch.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:opencanteen/lang/lang_cz.dart'; import 'package:opencanteen/loginmanager.dart'; @@ -16,6 +16,8 @@ import 'package:opencanteen/util.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:intl/intl.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; import 'lang/lang.dart'; import 'lang/lang_en.dart'; @@ -41,20 +43,18 @@ Copyright (C) 2022 Matyáš Caras a přispěvatelé final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); -void oznamitPredem(SharedPreferences prefs, {Languages? l}) async { +void oznamitPredem(SharedPreferences prefs, tz.Location l) async { String title; - if (l == null) { - String locale = Intl.getCurrentLocale(); - switch (locale) { - case "cs_CZ": - title = LanguageCz().lunchNotif; - break; - default: - title = LanguageEn().lunchNotif; - } - } else { - title = l.lunchNotif; + + String locale = Intl.getCurrentLocale(); + switch (locale) { + case "cs_CZ": + title = LanguageCz().lunchNotif; + break; + default: + title = LanguageEn().lunchNotif; } + if (prefs.getBool("offline") ?? false) { // TODO možnost brát z offline dat } else { @@ -66,21 +66,28 @@ void oznamitPredem(SharedPreferences prefs, {Languages? l}) async { var jidla = await c.jidelnicekDen(); try { var jidlo = jidla.jidla.singleWhere((element) => element.objednano); - var ted = TimeOfDay.now(); - var kdy = prefs.getString("oznameni_cas"); - var cas = TimeOfDay.fromDateTime(DateTime.parse(kdy!)); - if (ted.hour == cas.hour && ted.minute == cas.minute) { - const AndroidNotificationDetails androidPlatformChannelSpecifics = + var kdy = DateTime.parse(prefs.getString("oznameni_cas")!); + const AndroidNotificationDetails androidSpec = AndroidNotificationDetails('opencanteen', 'predobjedem', channelDescription: 'Oznámení o dnešním jídle', importance: Importance.max, priority: Priority.high, ticker: 'today meal'); - const NotificationDetails platformChannelSpecifics = - NotificationDetails(android: androidPlatformChannelSpecifics); - await flutterLocalNotificationsPlugin.show( - 0, title, "${jidlo.nazev} - ${jidlo.varianta}", platformChannelSpecifics); - } + const IOSNotificationDetails iOSpec = + IOSNotificationDetails(presentAlert: true, presentBadge: true); + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + title, + "${jidlo.nazev} - ${jidlo.varianta}", + tz.TZDateTime.from( + casNaDate( + TimeOfDay(hour: kdy.hour, minute: kdy.minute), + ), + l), + const NotificationDetails(android: androidSpec, iOS: iOSpec), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime); } on StateError catch (_) { // nenalezeno } @@ -89,46 +96,29 @@ void oznamitPredem(SharedPreferences prefs, {Languages? l}) async { } } -// Pouze pro Android -void backgroundFetchHeadlessTask(HeadlessTask task) async { - String taskId = task.taskId; - bool isTimeout = task.timeout; - if (isTimeout) { - // Timeout - debugPrint("[BackgroundFetch] Headless task má time-out: $taskId"); - BackgroundFetch.finish(taskId); - return; - } - var prefs = await SharedPreferences.getInstance(); - debugPrint('[BackgroundFetch] Přišel headless event.'); - - if (prefs.getBool("oznamit") ?? false) { - // Oznámení před obědem - oznamitPredem(prefs); - } - BackgroundFetch.finish(taskId); -} - void main() async { WidgetsFlutterBinding.ensureInitialized(); + tz.initializeTimeZones(); + var l = tz.getLocation(await FlutterNativeTimezone.getLocalTimezone()); + tz.setLocalLocation(l); + + var prefs = await SharedPreferences.getInstance(); + if (prefs.getBool("oznamit") ?? false) { + oznamitPredem(prefs, l); + } + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('notif_icon'); - /// Note: permissions aren't requested here just to demonstrate that can be - /// done later final IOSInitializationSettings initializationSettingsIOS = - IOSInitializationSettings( - requestAlertPermission: false, - requestBadgePermission: false, - requestSoundPermission: false, - onDidReceiveLocalNotification: ( - int id, - String? title, - String? body, - String? payload, - ) async { - debugPrint(body); - }); + IOSInitializationSettings(onDidReceiveLocalNotification: ( + int id, + String? title, + String? body, + String? payload, + ) async { + debugPrint(body); + }); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, @@ -141,7 +131,6 @@ void main() async { } }); runApp(const MyApp()); - BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); } class MyApp extends StatelessWidget { @@ -182,32 +171,6 @@ class _LoginPageState extends State { TextEditingController canteenControl = TextEditingController(); bool rememberMe = false; - void nastavitPozadi() async { - // Configure BackgroundFetch. - int status = await BackgroundFetch.configure( - BackgroundFetchConfig( - minimumFetchInterval: 15, - stopOnTerminate: false, - enableHeadless: true, - requiresBatteryNotLow: false, - requiresCharging: false, - requiresStorageNotLow: false, - requiresDeviceIdle: false, - requiredNetworkType: NetworkType.ANY), (String taskId) async { - // Callback - debugPrint("[BackgroundFetch] Event získán $taskId"); - var d = await LoginManager.getDetails(); - if (d != null) { - // TODO - } - BackgroundFetch.finish(taskId); - }, (String taskId) async { - debugPrint("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); - BackgroundFetch.finish(taskId); - }); - debugPrint('[BackgroundFetch] úspěšně nakonfigurováno: $status'); - } - @override void initState() { super.initState(); @@ -254,19 +217,18 @@ class _LoginPageState extends State { } const storage = FlutterSecureStorage(); var odsouhlasil = await storage.read(key: "oc_souhlas"); - nastavitPozadi(); if (odsouhlasil == null || odsouhlasil != "ano") { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (c) => WelcomeScreen(canteen: canteen))); + builder: (c) => WelcomeScreen( + canteen: canteen, n: flutterLocalNotificationsPlugin))); } else { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => JidelnicekPage( - canteen: canteen, - )), + canteen: canteen, n: flutterLocalNotificationsPlugin)), ); } } on PlatformException { @@ -392,7 +354,6 @@ class _LoginPageState extends State { LoginManager.setDetails(userControl.text, passControl.text, canteenControl.text); } - nastavitPozadi(); // souhlas const storage = FlutterSecureStorage(); var odsouhlasil = @@ -401,15 +362,16 @@ class _LoginPageState extends State { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (c) => - WelcomeScreen(canteen: canteen))); + builder: (c) => WelcomeScreen( + canteen: canteen, + n: flutterLocalNotificationsPlugin))); } else { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => JidelnicekPage( - canteen: canteen, - )), + canteen: canteen, + n: flutterLocalNotificationsPlugin)), ); } } on PlatformException { diff --git a/lib/okna/burza.dart b/lib/okna/burza.dart index f145532..fce72a3 100644 --- a/lib/okna/burza.dart +++ b/lib/okna/burza.dart @@ -1,13 +1,16 @@ import 'package:canteenlib/canteenlib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:opencanteen/util.dart'; import '../lang/lang.dart'; import '../main.dart'; class BurzaPage extends StatefulWidget { - const BurzaPage({Key? key, required this.canteen}) : super(key: key); + const BurzaPage({Key? key, required this.canteen, required this.n}) + : super(key: key); final Canteen canteen; + final FlutterLocalNotificationsPlugin n; @override State createState() => _BurzaPageState(); } @@ -112,7 +115,7 @@ class _BurzaPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - drawer: drawerGenerator(context, widget.canteen, 3), + drawer: drawerGenerator(context, widget.canteen, 3, widget.n), appBar: AppBar( title: Text(Languages.of(context)!.exchange), ), diff --git a/lib/okna/jidelnicek.dart b/lib/okna/jidelnicek.dart index d9f7524..e1e74b9 100644 --- a/lib/okna/jidelnicek.dart +++ b/lib/okna/jidelnicek.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:canteenlib/canteenlib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:opencanteen/okna/nastaveni.dart'; import 'package:opencanteen/util.dart'; @@ -15,8 +16,10 @@ import '../main.dart'; import 'about.dart'; class JidelnicekPage extends StatefulWidget { - const JidelnicekPage({Key? key, required this.canteen}) : super(key: key); + const JidelnicekPage({Key? key, required this.canteen, required this.n}) + : super(key: key); final Canteen canteen; + final FlutterLocalNotificationsPlugin n; @override State createState() => _JidelnicekPageState(); } @@ -301,7 +304,8 @@ class _JidelnicekPageState extends State { }); } - void kliknuti(String value, BuildContext context) { + void kliknuti( + String value, BuildContext context, FlutterLocalNotificationsPlugin n) { if (value == Languages.of(context)!.signOut) { const storage = FlutterSecureStorage(); storage.deleteAll(); @@ -314,7 +318,7 @@ class _JidelnicekPageState extends State { context, MaterialPageRoute(builder: (c) => const AboutPage())); } else if (value == Languages.of(context)!.settings) { Navigator.push( - context, MaterialPageRoute(builder: (c) => const Nastaveni())); + context, MaterialPageRoute(builder: (c) => Nastaveni(n: n))); } } @@ -365,12 +369,12 @@ class _JidelnicekPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - drawer: drawerGenerator(context, widget.canteen, 1), + drawer: drawerGenerator(context, widget.canteen, 1, widget.n), appBar: AppBar( title: Text(Languages.of(context)!.menu), actions: [ PopupMenuButton( - onSelected: ((String value) => kliknuti(value, context)), + onSelected: ((String value) => kliknuti(value, context, widget.n)), itemBuilder: (BuildContext context) { return { Languages.of(context)!.reportBugs, diff --git a/lib/okna/nastaveni.dart b/lib/okna/nastaveni.dart index d65ac6a..ea81d45 100644 --- a/lib/okna/nastaveni.dart +++ b/lib/okna/nastaveni.dart @@ -1,13 +1,22 @@ import 'dart:io'; +import 'package:canteenlib/canteenlib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; import '../lang/lang.dart'; +import '../loginmanager.dart'; +import '../util.dart'; class Nastaveni extends StatefulWidget { - const Nastaveni({Key? key}) : super(key: key); + const Nastaveni({Key? key, required this.n}) : super(key: key); + + final FlutterLocalNotificationsPlugin n; @override State createState() => _NastaveniState(); @@ -18,7 +27,7 @@ class _NastaveniState extends State { bool _preskakovatVikend = false; bool _kontrolovatTyden = false; bool _oznameniObed = false; - String? _oznameniCas; + TimeOfDay _oznameniCas = TimeOfDay.now(); void najitNastaveni() async { var preferences = await SharedPreferences.getInstance(); @@ -27,7 +36,14 @@ class _NastaveniState extends State { _preskakovatVikend = preferences.getBool("skip") ?? false; _kontrolovatTyden = preferences.getBool("tyden") ?? false; _oznameniObed = preferences.getBool("oznamit") ?? false; - _oznameniCas = preferences.getString("oznameni_cas"); + var _casStr = preferences.getString("oznameni_cas"); + if (_casStr == null) { + var now = DateTime.now(); + _oznameniCas = TimeOfDay.fromDateTime(DateTime.now()); + preferences.setString("oznameni_cas", now.toString()); + } else { + _oznameniCas = TimeOfDay.fromDateTime(DateTime.parse(_casStr)); + } }); } @@ -99,23 +115,82 @@ class _NastaveniState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(Languages.of(context)!.skipWeekend), + Flexible(child: Text(Languages.of(context)!.notifyLunch)), Switch( value: _oznameniObed, onChanged: (value) { setState(() { _oznameniObed = value; - zmenitNastaveni("oznamit", value); + zmenitNastaveni("oznamit", + value); // TODO: změnit pouze s uloženými údaji }); }) ], ), Text(Languages.of(context)!.notifyAt), - TimePickerDialog( - initialTime: (_oznameniCas == null) - ? TimeOfDay.now() - : TimeOfDay.fromDateTime(DateTime.parse(_oznameniCas!)), - ) + TextButton( + style: ButtonStyle( + textStyle: MaterialStateProperty.all(TextStyle( + color: (_oznameniObed) ? Colors.grey : Colors.purple))), + onPressed: () async { + if (_oznameniObed) { + var cas = await showTimePicker( + context: context, initialTime: _oznameniCas); + if (cas != null) { + var prefs = await SharedPreferences.getInstance(); + prefs.setString( + "oznameni_cas", + casNaDate(cas) + .toString()); // aktualizovat vybraný čas + var d = await LoginManager.getDetails(); // získat údaje + if (d != null) { + // Nové oznámení + var c = Canteen(d["url"]!); + if (await c.login(d["user"]!, d["pass"]!)) { + var jidla = await c.jidelnicekDen(); + try { + var jidlo = jidla.jidla + .singleWhere((element) => element.objednano); + + const AndroidNotificationDetails androidSpec = + AndroidNotificationDetails( + 'opencanteen', 'predobjedem', + channelDescription: + 'Oznámení o dnešním jídle', + importance: Importance.max, + priority: Priority.high, + ticker: 'today meal'); + const IOSNotificationDetails iOSpec = + IOSNotificationDetails( + presentAlert: true, presentBadge: true); + debugPrint(casNaDate(cas).toString()); + var l = tz.getLocation( + await FlutterNativeTimezone.getLocalTimezone()); + await widget.n.zonedSchedule( + // Vytvoří nové oznámení pro daný čas a datum + 0, + Languages.of(context)!.lunchNotif, + "${jidlo.nazev} - ${jidlo.varianta}", + tz.TZDateTime.from(casNaDate(cas), l), + const NotificationDetails( + android: androidSpec, iOS: iOSpec), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation + .absoluteTime); + } on StateError catch (_) { + // nenalezeno + } + } + } + } + setState(() { + _oznameniCas = cas ?? _oznameniCas; + }); + } + }, + child: Text( + "${(_oznameniCas.hour < 10 ? "0" : "") + _oznameniCas.hour.toString()}:${(_oznameniCas.minute < 10 ? "0" : "") + _oznameniCas.minute.toString()}")), ], ), )), diff --git a/lib/okna/offline_jidelnicek.dart b/lib/okna/offline_jidelnicek.dart index 69cceb6..973874e 100644 --- a/lib/okna/offline_jidelnicek.dart +++ b/lib/okna/offline_jidelnicek.dart @@ -87,9 +87,6 @@ class _OfflineJidelnicekState extends State { } else if (value == Languages.of(context)!.about) { Navigator.push( context, MaterialPageRoute(builder: (c) => const AboutPage())); - } else if (value == Languages.of(context)!.settings) { - Navigator.push( - context, MaterialPageRoute(builder: (c) => const Nastaveni())); } } @@ -111,7 +108,6 @@ class _OfflineJidelnicekState extends State { itemBuilder: (BuildContext context) { return { Languages.of(context)!.reportBugs, - Languages.of(context)!.settings, Languages.of(context)!.about, Languages.of(context)!.signOut }.map((String choice) { diff --git a/lib/okna/welcome.dart b/lib/okna/welcome.dart index 04639b5..36d555c 100644 --- a/lib/okna/welcome.dart +++ b/lib/okna/welcome.dart @@ -1,14 +1,17 @@ import 'package:canteenlib/canteenlib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'package:opencanteen/lang/lang.dart'; import 'package:opencanteen/okna/jidelnicek.dart'; class WelcomeScreen extends StatefulWidget { - const WelcomeScreen({Key? key, required this.canteen}) : super(key: key); + const WelcomeScreen({Key? key, required this.canteen, required this.n}) + : super(key: key); final Canteen canteen; + final FlutterLocalNotificationsPlugin n; @override State createState() => _WelcomeScreenState(); @@ -67,7 +70,8 @@ class _WelcomeScreenState extends State { const storage = FlutterSecureStorage(); await storage.write(key: "oc_souhlas", value: "ano"); Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (c) => JidelnicekPage(canteen: widget.canteen))); + builder: (c) => + JidelnicekPage(canteen: widget.canteen, n: widget.n))); }, ), ); diff --git a/lib/util.dart b/lib/util.dart index 8035ea7..1eccf9f 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -1,11 +1,13 @@ import 'package:canteenlib/canteenlib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:opencanteen/okna/burza.dart'; import 'lang/lang.dart'; import 'okna/jidelnicek.dart'; -Drawer drawerGenerator(BuildContext context, Canteen canteen, int p) { +Drawer drawerGenerator(BuildContext context, Canteen canteen, int p, + FlutterLocalNotificationsPlugin n) { Drawer drawer = const Drawer(); switch (p) { case 1: @@ -28,7 +30,7 @@ Drawer drawerGenerator(BuildContext context, Canteen canteen, int p) { onTap: () => Navigator.push( context, MaterialPageRoute( - builder: (context) => BurzaPage(canteen: canteen), + builder: (context) => BurzaPage(canteen: canteen, n: n), ), ), ), @@ -50,7 +52,7 @@ Drawer drawerGenerator(BuildContext context, Canteen canteen, int p) { onTap: () => Navigator.push( context, MaterialPageRoute( - builder: (c) => JidelnicekPage(canteen: canteen))), + builder: (c) => JidelnicekPage(canteen: canteen, n: n))), ), ListTile( leading: const Icon(Icons.store), @@ -81,3 +83,10 @@ class OfflineJidlo { required this.naBurze, required this.den}); } + +/// Vytvoří [DateTime] z [TimeOfDay] +DateTime casNaDate(TimeOfDay c) { + var now = DateTime.now(); + return DateTime.parse( + "${now.year}-${(now.month < 10 ? "0" : "") + now.month.toString()}-${(now.day < 10 ? "0" : "") + now.day.toString()} ${(c.hour < 10 ? "0" : "") + c.hour.toString()}:${(c.minute < 10 ? "0" : "") + c.minute.toString()}:00"); +} diff --git a/pubspec.lock b/pubspec.lock index 6d92ce2..b1df825 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,13 +22,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" - background_fetch: - dependency: "direct main" - description: - name: background_fetch - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" canteenlib: dependency: "direct main" description: @@ -144,6 +137,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_native_timezone: + dependency: "direct main" + description: + name: flutter_native_timezone + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" flutter_secure_storage: dependency: "direct main" description: @@ -421,7 +421,7 @@ packages: source: hosted version: "1.2.0" timezone: - dependency: transitive + dependency: "direct main" description: name: timezone url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index a5fbbf7..816a923 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,8 @@ dependencies: shared_preferences: ^2.0.13 introduction_screen: ^3.0.1 flutter_local_notifications: ^9.5.3+1 - background_fetch: ^1.1.0 + timezone: ^0.8.0 + flutter_native_timezone: ^2.0.0 dev_dependencies: flutter_lints: ^1.0.0 From 119f1000e2f1746f9188b7032a08535c4e1e9f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Wed, 8 Jun 2022 17:18:29 +0200 Subject: [PATCH 4/6] fix: aktualizovat licence --- lib/okna/about.dart | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/okna/about.dart b/lib/okna/about.dart index 59c95a5..f01b4fb 100644 --- a/lib/okna/about.dart +++ b/lib/okna/about.dart @@ -43,11 +43,6 @@ class _AboutPageState extends State { "Copyright 2017 German Saprykin. All rights reserved, licence BSD 3-Clause", "https://github.com/mogol/flutter_secure_storage/blob/develop/flutter_secure_storage/LICENSE"), const SizedBox(height: 10), - cudlik( - "connectivity_plus", - "Copyright 2017 The Chromium Authors. All rights reserved, licence BSD 3-Clause", - "https://github.com/fluttercommunity/plus_plugins/blob/main/packages/connectivity_plus/connectivity_plus/LICENSE"), - const SizedBox(height: 10), cudlik( "url_launcher", "Copyright 2013 The Flutter Authors. All rights reserved, licence BSD 3-Clause", @@ -60,18 +55,33 @@ class _AboutPageState extends State { const SizedBox(height: 10), cudlik( "path_provider", - "Copyright 2013 The Flutter Authors. All rights reserved., licence BSD-3-Clause", + "Copyright 2013 The Flutter Authors. All rights reserved, licence BSD-3-Clause", "https://github.com/flutter/plugins/blob/main/packages/path_provider/path_provider/LICENSE"), const SizedBox(height: 10), cudlik( "shared_preferences", - "Copyright 2013 The Flutter Authors. All rights reserved., licence BSD-3-Clause", + "Copyright 2013 The Flutter Authors. All rights reserved, licence BSD-3-Clause", "https://github.com/flutter/plugins/blob/main/packages/path_provider/path_provider/LICENSE"), const SizedBox(height: 10), cudlik( "introduction_screen", "Copyright 2019 Jean-Charles Moussé, licence MIT", - "https://github.com/Pyozer/introduction_screen/blob/master/LICENSE") + "https://github.com/Pyozer/introduction_screen/blob/master/LICENSE"), + const SizedBox(height: 10), + cudlik( + "flutter_local_notifications", + "Copyright 2018 Michael Bui. All rights reserved, licence BSD-3-Clause", + "https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/LICENSE"), + const SizedBox(height: 10), + cudlik( + "timezone", + "Copyright 2014, timezone project authors, licence BSD-2-Clause", + "https://github.com/srawlins/timezone/blob/master/LICENSE"), + const SizedBox(height: 10), + cudlik( + "flutter_native_timezone", + "Copyright 2019 pinkfish, licence Apache 2.0", + "https://github.com/pinkfish/flutter_native_timezone/blob/master/LICENSE"), ]), ), ), From b712f0e815505c0fe9ab345ada5f01578d65072c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Wed, 8 Jun 2022 19:06:30 +0200 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20poupravit=20chov=C3=A1n=C3=AD=20nast?= =?UTF-8?q?aven=C3=AD=20+=20tla=C4=8D=C3=ADtko=20dnes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + android/app/src/main/AndroidManifest.xml | 3 +- lib/lang/lang.dart | 4 + lib/lang/lang_cz.dart | 7 + lib/lang/lang_en.dart | 9 +- lib/okna/jidelnicek.dart | 27 ++-- lib/okna/nastaveni.dart | 157 +++++++++++++---------- pubspec.yaml | 2 +- 8 files changed, 132 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e7e417..326172f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 1.3.0 - Odstranit connectivity_plus - Přidat možnost oznámení s info o obědu v daný čas +- Přidat k jídelníčku tlačítko, které zobrazí dnešní jídelníček # 1.2.0 - Přidat možnost zobrazení oznámení v případě neobjednaného jídla na příští týden - Přidat oznámení o rozbitých uložených údajích diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 49bb8be..58aa68e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,8 @@ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" - android:screenOrientation="portrait"> + android:screenOrientation="portrait" + android:showWhenLocked="true">